From 078051237241f2b63f306623954c443d9c9913e9 Mon Sep 17 00:00:00 2001
From: Anna Hughes <annaraehughes@live.co.uk>
Date: Mon, 9 Dec 2024 17:45:30 +0000
Subject: [PATCH] fix: resource user routes (#726)

---
 src/entities/resource-feedback.entity.ts      |  3 +-
 src/entities/resource-user.entity.ts          |  4 +-
 src/entities/resource.entity.ts               |  4 +-
 .../dtos/create-resource-user.dto.ts          | 15 ----
 src/resource-user/dtos/resource-user.dto.ts   | 18 ++++
 .../dtos/update-resource-user.dto.ts          | 10 ++-
 src/resource-user/resource-user.controller.ts | 70 +++++++++-------
 src/resource-user/resource-user.module.ts     |  2 +
 src/resource-user/resource-user.service.ts    | 83 +++++++++++++++++--
 src/resource/resource.interface.ts            | 14 ++++
 src/resource/resource.service.ts              |  3 +
 .../dtos/create-session-user.dto.ts           | 10 ---
 src/user/dtos/get-user.dto.ts                 |  2 +
 src/user/user.service.ts                      |  2 +
 src/utils/serialize.ts                        | 18 ++++
 15 files changed, 190 insertions(+), 68 deletions(-)
 delete mode 100644 src/resource-user/dtos/create-resource-user.dto.ts
 create mode 100644 src/resource-user/dtos/resource-user.dto.ts
 create mode 100644 src/resource/resource.interface.ts
 delete mode 100644 src/session-user/dtos/create-session-user.dto.ts

diff --git a/src/entities/resource-feedback.entity.ts b/src/entities/resource-feedback.entity.ts
index 2479daca..7eb91034 100644
--- a/src/entities/resource-feedback.entity.ts
+++ b/src/entities/resource-feedback.entity.ts
@@ -1,4 +1,4 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
+import { Column, Entity, JoinTable, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
 import { FEEDBACK_TAGS_ENUM } from '../utils/constants';
 import { BaseBloomEntity } from './base.entity';
 import { ResourceEntity } from './resource.entity';
@@ -14,6 +14,7 @@ export class ResourceFeedbackEntity extends BaseBloomEntity {
   @ManyToOne(() => ResourceEntity, (resourceEntity) => resourceEntity.resourceFeedback, {
     onDelete: 'CASCADE',
   })
+  @JoinTable({ name: 'resource', joinColumn: { name: 'resourceId' } })
   resource: ResourceEntity;
 
   @Column()
diff --git a/src/entities/resource-user.entity.ts b/src/entities/resource-user.entity.ts
index cf3d284a..0c707a15 100644
--- a/src/entities/resource-user.entity.ts
+++ b/src/entities/resource-user.entity.ts
@@ -1,4 +1,4 @@
-import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
+import { Column, Entity, JoinTable, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
 import { BaseBloomEntity } from './base.entity';
 import { ResourceEntity } from './resource.entity';
 import { UserEntity } from './user.entity';
@@ -16,8 +16,10 @@ export class ResourceUserEntity extends BaseBloomEntity {
   @ManyToOne(() => ResourceEntity, (resourceEntity) => resourceEntity.resourceUser, {
     onDelete: 'CASCADE',
   })
+  @JoinTable({ name: 'resource', joinColumn: { name: 'resourceId' } })
   resource: ResourceEntity;
 
   @ManyToOne(() => UserEntity, (userEntity) => userEntity.resourceUser, { onDelete: 'CASCADE' })
+  @JoinTable({ name: 'user', joinColumn: { name: 'userId' } })
   user: UserEntity;
 }
diff --git a/src/entities/resource.entity.ts b/src/entities/resource.entity.ts
index 3699c03c..e0876c6a 100644
--- a/src/entities/resource.entity.ts
+++ b/src/entities/resource.entity.ts
@@ -1,4 +1,4 @@
-import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
+import { Column, Entity, JoinTable, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
 import { RESOURCE_CATEGORIES, STORYBLOK_STORY_STATUS_ENUM } from '../utils/constants';
 import { BaseBloomEntity } from './base.entity';
 import { ResourceFeedbackEntity } from './resource-feedback.entity';
@@ -40,6 +40,7 @@ export class ResourceEntity extends BaseBloomEntity {
   @OneToMany(() => ResourceUserEntity, (resourceUserEntity) => resourceUserEntity.resource, {
     cascade: true,
   })
+  @JoinTable({ name: 'resourceUser', joinColumn: { name: 'resourceUserId' } })
   resourceUser: ResourceUserEntity[];
 
   @OneToMany(
@@ -49,5 +50,6 @@ export class ResourceEntity extends BaseBloomEntity {
       cascade: true,
     },
   )
+  @JoinTable({ name: 'resourceFeedback', joinColumn: { name: 'resourceFeedbackId' } })
   resourceFeedback: ResourceFeedbackEntity[];
 }
diff --git a/src/resource-user/dtos/create-resource-user.dto.ts b/src/resource-user/dtos/create-resource-user.dto.ts
deleted file mode 100644
index 67316fc2..00000000
--- a/src/resource-user/dtos/create-resource-user.dto.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { IsDate, IsNotEmpty, IsOptional, IsString } from 'class-validator';
-
-export class CreateResourceUserDto {
-  @IsNotEmpty()
-  @IsString()
-  resourceId: string;
-
-  @IsNotEmpty()
-  @IsString()
-  userId: string;
-
-  @IsOptional()
-  @IsDate()
-  completedAt?: Date;
-}
diff --git a/src/resource-user/dtos/resource-user.dto.ts b/src/resource-user/dtos/resource-user.dto.ts
new file mode 100644
index 00000000..b70f42e5
--- /dev/null
+++ b/src/resource-user/dtos/resource-user.dto.ts
@@ -0,0 +1,18 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';
+
+export class ResourceUserDto {
+  @IsNotEmpty()
+  @IsString()
+  @ApiProperty({ type: String })
+  resourceId: string;
+
+  @IsNotEmpty()
+  @IsString()
+  @ApiProperty({ type: String })
+  userId: string;
+
+  @IsBoolean()
+  @ApiProperty({ type: Date })
+  completedAt?: Date;
+}
diff --git a/src/resource-user/dtos/update-resource-user.dto.ts b/src/resource-user/dtos/update-resource-user.dto.ts
index c1f8cf36..8e8c6b8e 100644
--- a/src/resource-user/dtos/update-resource-user.dto.ts
+++ b/src/resource-user/dtos/update-resource-user.dto.ts
@@ -1,7 +1,9 @@
-import { IsDate, IsOptional } from 'class-validator';
+import { ApiProperty } from '@nestjs/swagger';
+import { IsDefined, IsNotEmpty } from 'class-validator';
 
 export class UpdateResourceUserDto {
-  @IsOptional()
-  @IsDate()
-  completedAt?: Date;
+  @IsNotEmpty()
+  @IsDefined()
+  @ApiProperty({ type: Number })
+  storyblokId: number;
 }
diff --git a/src/resource-user/resource-user.controller.ts b/src/resource-user/resource-user.controller.ts
index 0b61ef16..dbf9efc9 100644
--- a/src/resource-user/resource-user.controller.ts
+++ b/src/resource-user/resource-user.controller.ts
@@ -1,47 +1,61 @@
-import {
-  Body,
-  Controller,
-  HttpException,
-  Param,
-  Patch,
-  Post,
-  Req,
-  UseGuards,
-} from '@nestjs/common';
-import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
-import { FirebaseAuthGuard } from 'src/firebase/firebase-auth.guard';
-import { CreateResourceUserDto } from './dtos/create-resource-user.dto';
+import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common';
+import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
+import { Request } from 'express';
+import { ControllerDecorator } from 'src/utils/controller.decorator';
+import { UserEntity } from '../entities/user.entity';
+import { FirebaseAuthGuard } from '../firebase/firebase-auth.guard';
 import { UpdateResourceUserDto } from './dtos/update-resource-user.dto';
 import { ResourceUserService } from './resource-user.service';
 
-@Controller('v1/resource-user')
+@ApiTags('Resource User')
+@ControllerDecorator()
+@Controller('/v1/resource-user')
 export class ResourceUserController {
   constructor(private readonly resourceUserService: ResourceUserService) {}
 
   @Post()
   @ApiBearerAuth('access-token')
   @ApiOperation({
-    description: 'Updates resource_user table',
+    description:
+      'Stores relationship between a `User` and `Resource` records, once a user has started a resource.',
   })
   @UseGuards(FirebaseAuthGuard)
-  create(@Req() req: Request, @Body() createResourceUserDto: CreateResourceUserDto) {
-    if (req['userEntity'].id !== createResourceUserDto.userId) {
-      throw new HttpException('Unauthorized', 401);
-    }
-    return this.resourceUserService.create(createResourceUserDto);
+  async createResourceUser(
+    @Req() req: Request,
+    @Body() createResourceUserDto: UpdateResourceUserDto,
+  ) {
+    return await this.resourceUserService.createResourceUser(
+      req['userEntity'] as UserEntity,
+      createResourceUserDto,
+    );
   }
 
-  @Patch(':id')
+  @Post('/complete')
+  @ApiOperation({
+    description: 'Updates a users resources progress to completed',
+  })
   @ApiBearerAuth('access-token')
+  @UseGuards(FirebaseAuthGuard)
+  async complete(@Req() req: Request, @Body() updateResourceUserDto: UpdateResourceUserDto) {
+    return await this.resourceUserService.setResourceUserCompleted(
+      req['userEntity'] as UserEntity,
+      updateResourceUserDto,
+      true,
+    );
+  }
+
+  @Post('/incomplete')
   @ApiOperation({
-    description: 'Updates resource_user table',
+    description:
+      'Updates a users resources progress to incomplete, undoing a previous complete action',
   })
+  @ApiBearerAuth('access-token')
   @UseGuards(FirebaseAuthGuard)
-  update(
-    @Req() req: Request,
-    @Param('id') id: string,
-    @Body() updateResourceUserDto: UpdateResourceUserDto,
-  ) {
-    return this.resourceUserService.update(id, updateResourceUserDto);
+  async incomplete(@Req() req: Request, @Body() updateResourceUserDto: UpdateResourceUserDto) {
+    return await this.resourceUserService.setResourceUserCompleted(
+      req['userEntity'] as UserEntity,
+      updateResourceUserDto,
+      false,
+    );
   }
 }
diff --git a/src/resource-user/resource-user.module.ts b/src/resource-user/resource-user.module.ts
index 4d4ef403..ef1fdf69 100644
--- a/src/resource-user/resource-user.module.ts
+++ b/src/resource-user/resource-user.module.ts
@@ -18,6 +18,7 @@ import { UserEntity } from 'src/entities/user.entity';
 import { EventLoggerService } from 'src/event-logger/event-logger.service';
 import { PartnerAccessService } from 'src/partner-access/partner-access.service';
 import { PartnerService } from 'src/partner/partner.service';
+import { ResourceService } from 'src/resource/resource.service';
 import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service';
 import { SubscriptionUserService } from 'src/subscription-user/subscription-user.service';
 import { SubscriptionService } from 'src/subscription/subscription.service';
@@ -46,6 +47,7 @@ import { ResourceUserService } from './resource-user.service';
   ],
   controllers: [ResourceUserController],
   providers: [
+    ResourceService,
     ResourceUserService,
     UserService,
     PartnerAccessService,
diff --git a/src/resource-user/resource-user.service.ts b/src/resource-user/resource-user.service.ts
index 33b8a55b..bc5c6de4 100644
--- a/src/resource-user/resource-user.service.ts
+++ b/src/resource-user/resource-user.service.ts
@@ -1,8 +1,11 @@
 import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
 import { InjectRepository } from '@nestjs/typeorm';
 import { ResourceUserEntity } from 'src/entities/resource-user.entity';
+import { UserEntity } from 'src/entities/user.entity';
+import { ResourceService } from 'src/resource/resource.service';
+import { formatResourceUserObject } from 'src/utils/serialize';
 import { Repository } from 'typeorm';
-import { CreateResourceUserDto } from './dtos/create-resource-user.dto';
+import { ResourceUserDto } from './dtos/resource-user.dto';
 import { UpdateResourceUserDto } from './dtos/update-resource-user.dto';
 
 @Injectable()
@@ -10,21 +13,85 @@ export class ResourceUserService {
   constructor(
     @InjectRepository(ResourceUserEntity)
     private resourceUserRepository: Repository<ResourceUserEntity>,
+    private resourceService: ResourceService,
   ) {}
 
-  create(createResourceUserDto: CreateResourceUserDto) {
-    return this.resourceUserRepository.save(createResourceUserDto);
+  private async getResourceUser({
+    resourceId,
+    userId,
+  }: ResourceUserDto): Promise<ResourceUserEntity> {
+    return await this.resourceUserRepository
+      .createQueryBuilder('resource_user')
+      .leftJoinAndSelect('resource_user.resource', 'resource')
+      .where('resource_user.userId = :userId', { userId })
+      .andWhere('resource_user.resourceId = :resourceId', { resourceId })
+      .getOne();
   }
 
-  update(id: string, updateResourceUserDto: UpdateResourceUserDto) {
-    const resourceUser = this.resourceUserRepository.findOne({ where: { id } });
+  async createResourceUserRecord({
+    resourceId,
+    userId,
+  }: ResourceUserDto): Promise<ResourceUserEntity> {
+    return await this.resourceUserRepository.save({
+      resourceId,
+      userId,
+      completedAt: null,
+    });
+  }
+
+  public async createResourceUser(user: UserEntity, { storyblokId }: UpdateResourceUserDto) {
+    const resource = await this.resourceService.getResourceByStoryblokId(storyblokId);
+
+    if (!resource) {
+      throw new HttpException('RESOURCE NOT FOUND', HttpStatus.NOT_FOUND);
+    }
+
+    let resourceUser = await this.getResourceUser({
+      resourceId: resource.id,
+      userId: user.id,
+    });
 
     if (!resourceUser) {
-      throw new HttpException('RESOURCE USER NOT FOUND', HttpStatus.NOT_FOUND);
+      resourceUser = await this.createResourceUserRecord({
+        resourceId: resource.id,
+        userId: user.id,
+      });
+    }
+
+    return formatResourceUserObject([{ ...resourceUser, resource }])[0];
+  }
+
+  public async setResourceUserCompleted(
+    user: UserEntity,
+    { storyblokId }: UpdateResourceUserDto,
+    completed: boolean,
+  ) {
+    const resource = await this.resourceService.getResourceByStoryblokId(storyblokId);
+
+    if (!resource) {
+      throw new HttpException(
+        `Resource not found for storyblok id: ${storyblokId}`,
+        HttpStatus.NOT_FOUND,
+      );
     }
 
-    const updatedResourceUser = { ...resourceUser, ...updateResourceUserDto };
+    let resourceUser = await this.getResourceUser({
+      resourceId: resource.id,
+      userId: user.id,
+    });
+
+    if (resourceUser) {
+      await this.resourceUserRepository.save({
+        ...resourceUser,
+        completedAt: completed ? new Date() : null,
+      });
+    } else {
+      resourceUser = await this.createResourceUserRecord({
+        resourceId: resource.id,
+        userId: user.id,
+      });
+    }
 
-    return this.resourceUserRepository.save(updatedResourceUser);
+    return formatResourceUserObject([{ ...resourceUser, resource }])[0];
   }
 }
diff --git a/src/resource/resource.interface.ts b/src/resource/resource.interface.ts
new file mode 100644
index 00000000..169ee489
--- /dev/null
+++ b/src/resource/resource.interface.ts
@@ -0,0 +1,14 @@
+import { RESOURCE_CATEGORIES, STORYBLOK_STORY_STATUS_ENUM } from 'src/utils/constants';
+
+export interface IResource {
+  id?: string;
+  createdAt?: Date | string;
+  updatedAt?: Date | string;
+  name?: string;
+  slug?: string;
+  status?: STORYBLOK_STORY_STATUS_ENUM;
+  storyblokId?: number;
+  storyblokUuid?: string;
+  category?: RESOURCE_CATEGORIES;
+  completedAt?: Date | string;
+}
diff --git a/src/resource/resource.service.ts b/src/resource/resource.service.ts
index cd3eb71e..e881da38 100644
--- a/src/resource/resource.service.ts
+++ b/src/resource/resource.service.ts
@@ -20,4 +20,7 @@ export class ResourceService {
   async create(createResourceDto: CreateResourceDto): Promise<ResourceEntity> {
     return this.resourceRepository.save(createResourceDto);
   }
+  async getResourceByStoryblokId(storyblokId: number): Promise<ResourceEntity> {
+    return await this.resourceRepository.findOneBy({ storyblokId: storyblokId });
+  }
 }
diff --git a/src/session-user/dtos/create-session-user.dto.ts b/src/session-user/dtos/create-session-user.dto.ts
deleted file mode 100644
index 60676928..00000000
--- a/src/session-user/dtos/create-session-user.dto.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsDefined, IsNotEmpty, IsString } from 'class-validator';
-
-export class CreateSessionUserDto {
-  @IsString()
-  @IsNotEmpty()
-  @IsDefined()
-  @ApiProperty({ type: String })
-  sessionId: string;
-}
diff --git a/src/user/dtos/get-user.dto.ts b/src/user/dtos/get-user.dto.ts
index 6aecbb85..f534942c 100644
--- a/src/user/dtos/get-user.dto.ts
+++ b/src/user/dtos/get-user.dto.ts
@@ -1,4 +1,5 @@
 import { ICoursesWithSessions } from 'src/course/course.interface';
+import { IResource } from 'src/resource/resource.interface';
 import { ITherapySession } from 'src/webhooks/webhooks.interface';
 import { IPartnerAccessWithPartner } from '../../partner-access/partner-access.interface';
 import { IPartnerAdminWithPartner } from '../../partner-admin/partner-admin.interface';
@@ -10,6 +11,7 @@ export class GetUserDto {
   partnerAccesses?: IPartnerAccessWithPartner[];
   partnerAdmin?: IPartnerAdminWithPartner;
   courses?: ICoursesWithSessions[];
+  resources?: IResource[];
   therapySessions?: ITherapySession[];
   subscriptions?: ISubscriptionUser[];
 }
diff --git a/src/user/user.service.ts b/src/user/user.service.ts
index 95ba8f76..16758b6a 100644
--- a/src/user/user.service.ts
+++ b/src/user/user.service.ts
@@ -142,6 +142,8 @@ export class UserService {
       .leftJoinAndSelect('courseUser.course', 'course')
       .leftJoinAndSelect('courseUser.sessionUser', 'sessionUser')
       .leftJoinAndSelect('sessionUser.session', 'session')
+      .leftJoinAndSelect('user.resourceUser', 'resourceUser')
+      .leftJoinAndSelect('resourceUser.resource', 'resource')
       .leftJoinAndSelect('user.subscriptionUser', 'subscriptionUser')
       .leftJoinAndSelect('subscriptionUser.subscription', 'subscription')
       .where('user.id = :id', { id })
diff --git a/src/utils/serialize.ts b/src/utils/serialize.ts
index a97e4dc8..0d8f31b6 100644
--- a/src/utils/serialize.ts
+++ b/src/utils/serialize.ts
@@ -1,5 +1,6 @@
 import { PartnerAdminEntity } from 'src/entities/partner-admin.entity';
 import { PartnerEntity } from 'src/entities/partner.entity';
+import { ResourceUserEntity } from 'src/entities/resource-user.entity';
 import { IPartnerFeature } from 'src/partner-feature/partner-feature.interface';
 import { IPartner } from 'src/partner/partner.interface';
 import { GetSubscriptionUserDto } from 'src/subscription-user/dto/get-subscription-user.dto';
@@ -43,6 +44,22 @@ export const formatCourseUserObject = (courseUser: CourseUserEntity) => {
   };
 };
 
+export const formatResourceUserObject = (resourceUsers: ResourceUserEntity[]) => {
+  return resourceUsers.map((resourceUser) => {
+    return {
+      id: resourceUser.resource.id,
+      createdAt: resourceUser.createdAt,
+      updatedAt: resourceUser.updatedAt,
+      name: resourceUser.resource.name,
+      slug: resourceUser.resource.slug,
+      status: resourceUser.resource.status,
+      storyblokId: resourceUser.resource.storyblokId,
+      storyblokUuid: resourceUser.resource.storyblokUuid,
+      completed: !!resourceUser.completedAt, // convert to boolean from data populated
+    };
+  });
+};
+
 export const formatPartnerAdminObjects = (partnerAdminObject: PartnerAdminEntity) => {
   return {
     id: partnerAdminObject.id,
@@ -113,6 +130,7 @@ export const formatUserObject = (userObject: UserEntity): GetUserDto => {
       ? formatPartnerAdminObjects(userObject.partnerAdmin)
       : null,
     courses: userObject.courseUser ? formatCourseUserObjects(userObject.courseUser) : [],
+    resources: userObject.resourceUser ? formatResourceUserObject(userObject.resourceUser) : [],
     subscriptions:
       userObject.subscriptionUser && userObject.subscriptionUser.length > 0
         ? formatSubscriptionObjects(userObject.subscriptionUser)