diff --git a/docs/key-concepts.md b/docs/key-concepts.md index 6aa0554f..c425934d 100644 --- a/docs/key-concepts.md +++ b/docs/key-concepts.md @@ -18,11 +18,11 @@ User authentication is handled by [Firebase Auth](https://firebase.google.com/do Crisp is the messaging platform used to message the Chayn team in relation to bloom course content or other queries and support. For public users, this 1-1 chat feature is available on _live_ courses only. For partner users, This 1-1 chat feature is available to users with a `PartnerAccess` that has 1-1 chat enabled. -Users who have access to 1-1 chat also have a profile on Crisp that reflects data from our database regarding their partners, access and course progress. See [crisp-api.ts](src/api/crisp/crisp-api.ts) +Users who have access to 1-1 chat also have a profile on Crisp that reflects data from our database regarding their partners, access and course progress. See [crisp.service.ts](src/crisp/crisp.service.ts) ### Reporting Google Data Studio is an online tool for converting data into customizable informative reports and dashboards. The reports are generated by writing custom sql queries that return actionable data to Data Studio. Filters are applied in Data Studio allowing -data to be segregated into different time periods based on the data createdAt date \ No newline at end of file +data to be segregated into different time periods based on the data createdAt date diff --git a/package.json b/package.json index e9cd9efe..cc162fcd 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "axios": "^1.7.7", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "crisp-api": "^9.2.0", "date-fns": "^3.6.0", "dotenv": "^16.4.5", "firebase": "^10.10.0", diff --git a/src/api/crisp/crisp-api.ts b/src/api/crisp/crisp-api.ts deleted file mode 100644 index 43c63151..00000000 --- a/src/api/crisp/crisp-api.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { AxiosResponse } from 'axios'; -import { crispToken, crispWebsiteId } from '../../utils/constants'; -import apiCall from '../apiCalls'; -import { - CrispProfileBase, - CrispProfileBaseResponse, - CrispProfileCustomFields, - CrispProfileDataResponse, - NewCrispProfileBaseResponse, -} from './crisp-api.interfaces'; - -const baseUrl = `https://api.crisp.chat/v1/website/${crispWebsiteId}`; - -const headers = { - Authorization: `Basic ${crispToken}`, - 'X-Crisp-Tier': 'plugin', - '-ContentType': 'application/json', -}; - -export const createCrispProfile = async ( - newPeopleProfile: CrispProfileBase, -): Promise> => { - try { - return await apiCall({ - url: `${baseUrl}/people/profile`, - type: 'post', - data: newPeopleProfile, - headers, - }); - } catch (error) { - throw new Error(`Create crisp profile API call failed: ${error}`); - } -}; - -// Note getCrispProfile is not currently used -export const getCrispProfile = async ( - email: string, -): Promise> => { - try { - return await apiCall({ - url: `${baseUrl}/people/profile/${email}`, - type: 'get', - headers, - }); - } catch (error) { - throw new Error(`Get crisp profile base API call failed: ${error}`); - } -}; - -// Note getCrispPeopleData is not currently used -export const getCrispPeopleData = async ( - email: string, -): Promise> => { - try { - return await apiCall({ - url: `${baseUrl}/people/data/${email}`, - type: 'get', - headers, - }); - } catch (error) { - throw new Error(`Get crisp profile API call failed: ${error}`); - } -}; - -export const updateCrispProfileBase = async ( - peopleProfile: CrispProfileBase, - email: string, -): Promise> => { - try { - return await apiCall({ - url: `${baseUrl}/people/profile/${email}`, - type: 'patch', - data: peopleProfile, - headers, - }); - } catch (error) { - throw new Error(`Update crisp profile base API call failed: ${error}`); - } -}; - -export const updateCrispProfile = async ( - peopleData: CrispProfileCustomFields, - email: string, -): Promise> => { - try { - return await apiCall({ - url: `${baseUrl}/people/data/${email}`, - type: 'patch', - data: { data: peopleData }, - headers, - }); - } catch (error) { - throw new Error(`Update crisp profile API call failed: ${error}`); - } -}; - -export const deleteCrispProfile = async (email: string) => { - try { - await apiCall({ - url: `${baseUrl}/people/profile/${email}`, - type: 'delete', - headers, - }); - } catch (error) { - throw new Error(`Delete crisp profile API call failed: ${error}`); - } -}; - -export const deleteCypressCrispProfiles = async () => { - try { - const profiles = await apiCall({ - url: `${baseUrl}/people/profiles/1?search_text=cypresstestemail+`, - type: 'get', - headers, - }); - - profiles.data.data.forEach(async (profile) => { - await apiCall({ - url: `${baseUrl}/people/profile/${profile.email}`, - type: 'delete', - headers, - }); - }); - } catch (error) { - throw new Error(`Delete cypress crisp profiles API call failed: ${error}`); - } -}; diff --git a/src/api/mailchimp/mailchimp-api.interfaces.ts b/src/api/mailchimp/mailchimp-api.interfaces.ts index 76b6e03d..06b84b2a 100644 --- a/src/api/mailchimp/mailchimp-api.interfaces.ts +++ b/src/api/mailchimp/mailchimp-api.interfaces.ts @@ -14,6 +14,10 @@ export enum MAILCHIMP_MERGE_FIELD_TYPES { ZIP = 'zip', } +export enum MAILCHIMP_CUSTOM_EVENTS { + CRISP_MESSAGE_RECEIVED = 'CRISP_MESSAGE_RECEIVED', +} + export interface ListMemberCustomFields { NAME?: string; SIGNUPD?: string; diff --git a/src/api/mailchimp/mailchimp-api.ts b/src/api/mailchimp/mailchimp-api.ts index f9dbac9b..d4aa2bac 100644 --- a/src/api/mailchimp/mailchimp-api.ts +++ b/src/api/mailchimp/mailchimp-api.ts @@ -5,6 +5,7 @@ import { Logger } from '../../logger/logger'; import { ListMember, ListMemberPartial, + MAILCHIMP_CUSTOM_EVENTS, MAILCHIMP_MERGE_FIELD_TYPES, UpdateListMemberRequest, } from './mailchimp-api.interfaces'; @@ -128,3 +129,13 @@ export const deleteCypressMailchimpProfiles = async () => { throw new Error(`Delete cypress mailchimp profiles API call failed: ${error}`); } }; + +export const sendMailchimpUserEvent = async (email: string, event: MAILCHIMP_CUSTOM_EVENTS) => { + try { + await mailchimp.lists.createListMemberEvent(mailchimpAudienceId, getEmailMD5Hash(email), { + name: event, + }); + } catch (error) { + throw new Error(`Send mailchimp user event failed: ${error}`); + } +}; diff --git a/src/app.module.ts b/src/app.module.ts index ca87f501..09723755 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -6,6 +6,8 @@ import { AuthModule } from './auth/auth.module'; import { CoursePartnerModule } from './course-partner/course-partner.module'; import { CourseUserModule } from './course-user/course-user.module'; import { CourseModule } from './course/course.module'; +import { CrispListenerModule } from './crisp-listener/crisp-listener.module'; +import { CrispModule } from './crisp/crisp.module'; import { EventLoggerModule } from './event-logger/event-logger.module'; import { FeatureModule } from './feature/feature.module'; import { HealthModule } from './health/health.module'; @@ -43,6 +45,8 @@ import { WebhooksModule } from './webhooks/webhooks.module'; PartnerFeatureModule, EventLoggerModule, HealthModule, + CrispModule, + CrispListenerModule, ], }) export class AppModule {} diff --git a/src/crisp-listener/crisp-listener.module.ts b/src/crisp-listener/crisp-listener.module.ts new file mode 100644 index 00000000..5a7d9141 --- /dev/null +++ b/src/crisp-listener/crisp-listener.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CrispService } from 'src/crisp/crisp.service'; +import { EventLogEntity } from 'src/entities/event-log.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; +import { CrispListenerService } from './crisp-listener.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([EventLogEntity, UserEntity])], + providers: [CrispService, CrispListenerService, EventLoggerService], +}) +export class CrispListenerModule {} diff --git a/src/crisp-listener/crisp-listener.service.ts b/src/crisp-listener/crisp-listener.service.ts new file mode 100644 index 00000000..ded23d84 --- /dev/null +++ b/src/crisp-listener/crisp-listener.service.ts @@ -0,0 +1,48 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import Crisp from 'crisp-api'; +import { EVENT_NAME } from 'src/crisp/crisp.interface'; +import { CrispService } from 'src/crisp/crisp.service'; +import { CrispEventDto } from 'src/crisp/dtos/crisp.dto'; +import { crispPluginId, crispPluginKey } from 'src/utils/constants'; +const CrispClient = new Crisp(); +const logger = new Logger('CrispLogger'); + +// This service is split from CrispService due to CrispService being imported/initiated multiple times +// To avoid creating duplicate listeners and events, this CrispListenerService was decoupled +@Injectable() +export class CrispListenerService implements OnModuleInit { + constructor(private crispService: CrispService) { + CrispClient.authenticateTier('plugin', crispPluginId, crispPluginKey); + } + + onModuleInit() { + logger.log(`Crisp service initiated`); + + try { + const handleCrispEvent = async (message, eventName) => + await this.crispService.handleCrispEvent(message, eventName); + + CrispClient.on('message:send', async function (message: CrispEventDto) { + handleCrispEvent(message, EVENT_NAME.CHAT_MESSAGE_SENT); + }) + .then(function () { + logger.log('Crisp service listening to sent messages'); + }) + .catch(function (error) { + logger.error('Crisp service failed listening to sent messages:', error); + }); + + CrispClient.on('message:received', function (message: CrispEventDto) { + handleCrispEvent(message, EVENT_NAME.CHAT_MESSAGE_RECEIVED); + }) + .then(function () { + logger.log('Crisp service listening to received messages'); + }) + .catch(function (error) { + logger.error('Crisp service failed listening to sent messages:', error); + }); + } catch (error) { + logger.error('Crisp service failed to initiate:', error); + } + } +} diff --git a/src/api/crisp/crisp-api.interfaces.ts b/src/crisp/crisp.interface.ts similarity index 77% rename from src/api/crisp/crisp-api.interfaces.ts rename to src/crisp/crisp.interface.ts index 9b4f967f..4f5ab0cc 100644 --- a/src/api/crisp/crisp-api.interfaces.ts +++ b/src/crisp/crisp.interface.ts @@ -1,4 +1,17 @@ -import { EMAIL_REMINDERS_FREQUENCY } from '../../utils/constants'; +import { EMAIL_REMINDERS_FREQUENCY } from 'src/utils/constants'; + +export enum EVENT_NAME { + CHAT_MESSAGE_SENT = 'CHAT_MESSAGE_SENT', + CHAT_MESSAGE_RECEIVED = 'CHAT_MESSAGE_RECEIVED', + LOGGED_IN = 'LOGGED_IN', + LOGGED_OUT = 'LOGGED_OUT', +} + +export interface ICreateEventLog { + date: Date | string; + event: EVENT_NAME; + userId: string; +} export interface CrispProfileCustomFields { signed_up_at?: string; @@ -32,8 +45,8 @@ export interface CrispProfileCustomFields { export interface CrispProfileBase { email?: string; person?: { - nickname: string; - locales: string[]; + nickname?: string; + locales?: string[]; }; segments?: string[]; notepad?: string; diff --git a/src/crisp/crisp.module.ts b/src/crisp/crisp.module.ts new file mode 100644 index 00000000..d240140a --- /dev/null +++ b/src/crisp/crisp.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { EventLogEntity } from 'src/entities/event-log.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; +import { CrispService } from './crisp.service'; + +@Module({ + imports: [TypeOrmModule.forFeature([EventLogEntity, UserEntity])], + providers: [CrispService, EventLoggerService], +}) +export class CrispModule {} diff --git a/src/crisp/crisp.service.ts b/src/crisp/crisp.service.ts new file mode 100644 index 00000000..1a6c878a --- /dev/null +++ b/src/crisp/crisp.service.ts @@ -0,0 +1,143 @@ +import { Injectable, Logger } from '@nestjs/common'; +import Crisp from 'crisp-api'; +import { sendMailchimpUserEvent } from 'src/api/mailchimp/mailchimp-api'; +import { MAILCHIMP_CUSTOM_EVENTS } from 'src/api/mailchimp/mailchimp-api.interfaces'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; +import { crispPluginId, crispPluginKey, crispWebsiteId } from 'src/utils/constants'; +import { + CrispProfileBase, + CrispProfileBaseResponse, + CrispProfileCustomFields, + CrispProfileDataResponse, + EVENT_NAME, + NewCrispProfileBaseResponse, +} from './crisp.interface'; +import { CrispEventDto } from './dtos/crisp.dto'; + +const CrispClient = new Crisp(); +const logger = new Logger('CrispLogger'); + +@Injectable() +export class CrispService { + constructor(private eventLoggerService: EventLoggerService) { + CrispClient.authenticateTier('plugin', crispPluginId, crispPluginKey); + } + + async handleCrispEvent(message: CrispEventDto, eventName: EVENT_NAME) { + try { + const sessionMetaData = await CrispClient.website.getConversationMetas( + message.website_id, + message.session_id, + ); + await this.eventLoggerService.createEventLog({ + email: sessionMetaData.email, + event: eventName, + date: new Date(), + }); + + if (eventName === EVENT_NAME.CHAT_MESSAGE_RECEIVED) { + sendMailchimpUserEvent( + sessionMetaData.email, + MAILCHIMP_CUSTOM_EVENTS.CRISP_MESSAGE_RECEIVED, + ); + logger.log('Crisp service: CRISP_MESSAGE_RECEIVED event sent to mailchimp'); + } + } catch (error) { + throw new Error(`Failed to handle crisp event for ${eventName}: ${error}`); + } + } + + async createCrispProfile( + newPeopleProfile: CrispProfileBase, + ): Promise { + try { + const crispProfile = CrispClient.website.addNewPeopleProfile( + crispWebsiteId, + newPeopleProfile, + ); + return crispProfile; + } catch (error) { + throw new Error(`Create crisp profile API call failed: ${error}`); + } + } + + // Note getCrispProfile is not currently used + async getCrispProfile(email: string): Promise { + try { + const crispProfile = CrispClient.website.getPeopleProfile(crispWebsiteId, email); + return crispProfile; + } catch (error) { + throw new Error(`Get crisp profile base API call failed: ${error}`); + } + } + + // Note getCrispPeopleData is not currently used + async getCrispPeopleData(email: string): Promise { + try { + const crispPeopleData = CrispClient.website.getPeopleData(crispWebsiteId, email); + return crispPeopleData; + } catch (error) { + throw new Error(`Get crisp profile API call failed: ${error}`); + } + } + + async updateCrispProfileBase( + peopleProfile: CrispProfileBase, + email: string, + ): Promise { + try { + const crispProfile = CrispClient.website.updatePeopleProfile( + crispWebsiteId, + email, + peopleProfile, + ); + return crispProfile; + } catch (error) { + throw new Error(`Update crisp profile base API call failed: ${error}`); + } + } + + async updateCrispPeopleData( + peopleData: CrispProfileCustomFields, + email: string, + ): Promise { + try { + const crispPeopleData = CrispClient.website.updatePeopleData( + crispWebsiteId, + email, + peopleData, + ); + return crispPeopleData; + } catch (error) { + throw new Error(`Update crisp profile API call failed: ${error}`); + } + } + + async deleteCrispProfile(email: string) { + try { + CrispClient.website.removePeopleProfile(crispWebsiteId, email); + } catch (error) { + throw new Error(`Delete crisp profile API call failed: ${error}`); + } + } + + async deleteCypressCrispProfiles() { + try { + const profiles = CrispClient.website.listPeopleProfiles( + crispWebsiteId, + undefined, + undefined, + undefined, + undefined, + undefined, + 'cypresstestemail+', + ); + + profiles.data.data.forEach(async (profile) => { + CrispClient.website.removePeopleProfile(crispWebsiteId, profile.email); + }); + } catch (error) { + throw new Error(`Delete cypress crisp profiles API call failed: ${error}`); + } + } +} diff --git a/src/crisp/dtos/crisp.dto.ts b/src/crisp/dtos/crisp.dto.ts new file mode 100644 index 00000000..8880a227 --- /dev/null +++ b/src/crisp/dtos/crisp.dto.ts @@ -0,0 +1,68 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsBoolean, IsDefined, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { CrispProfileCustomFields } from '../crisp.interface'; + +export interface CrispUserData extends CrispProfileCustomFields { + email: string; + nickname: string; + user_id: string; +} + +export class CrispEventDto { + @IsNotEmpty() + @IsDefined() + @ApiProperty({ type: String }) + website_id: string; + + @IsNotEmpty() + @IsDefined() + @ApiProperty({ type: String }) + session_id: string; + + @IsOptional() + @ApiProperty({ type: String }) + inbox_id: string; + + @IsOptional() + @ApiProperty({ type: String }) + type: string; + + @IsOptional() + @ApiProperty({ type: String }) + origin: string; + + @IsNotEmpty() + @IsDefined() + @ApiProperty({ type: String }) + content: string; + + @IsNotEmpty() + @IsDefined() + @ApiProperty({ type: String }) + from: string; + + @IsNumber() + @IsNotEmpty() + @IsDefined() + @ApiProperty({ type: Number }) + timestamp: number; + + @IsNumber() + @IsNotEmpty() + @IsDefined() + @ApiProperty({ type: Number }) + fingerprint: number; + + @IsOptional() + @IsNumber() + space_id?: number; + + @IsOptional() + @IsString() + full_slug?: string; + + @IsBoolean() + stamped: boolean; + + user: CrispUserData; +} diff --git a/src/event-logger/event-logger.interface.ts b/src/event-logger/event-logger.interface.ts index 18c22834..3bdfc7cd 100644 --- a/src/event-logger/event-logger.interface.ts +++ b/src/event-logger/event-logger.interface.ts @@ -1,11 +1,13 @@ export enum EVENT_NAME { CHAT_MESSAGE_SENT = 'CHAT_MESSAGE_SENT', + CHAT_MESSAGE_RECEIVED = 'CHAT_MESSAGE_RECEIVED', LOGGED_IN = 'LOGGED_IN', LOGGED_OUT = 'LOGGED_OUT', } export interface ICreateEventLog { + email?: string; + userId?: string; date: Date | string; event: EVENT_NAME; - userId: string; } diff --git a/src/event-logger/event-logger.module.ts b/src/event-logger/event-logger.module.ts index db1062a3..64ac12ad 100644 --- a/src/event-logger/event-logger.module.ts +++ b/src/event-logger/event-logger.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { ZapierWebhookClient } from 'src/api/zapier/zapier-webhook-client'; +import { CrispService } from 'src/crisp/crisp.service'; import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; @@ -39,6 +40,7 @@ import { EventLoggerService } from './event-logger.service'; TherapySessionService, PartnerAccessService, SubscriptionService, + CrispService, ZapierWebhookClient, SlackMessageClient, ], diff --git a/src/event-logger/event-logger.service.spec.ts b/src/event-logger/event-logger.service.spec.ts index 31d8ec57..a66eeffa 100644 --- a/src/event-logger/event-logger.service.spec.ts +++ b/src/event-logger/event-logger.service.spec.ts @@ -1,8 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; +import { CrispService } from 'src/crisp/crisp.service'; import { EventLogEntity } from 'src/entities/event-log.entity'; -import { mockEventLoggerRepositoryMethods } from 'test/utils/mockedServices'; +import { UserEntity } from 'src/entities/user.entity'; +import { + mockEventLoggerRepositoryMethods, + mockUserRepositoryMethods, +} from 'test/utils/mockedServices'; import { Repository } from 'typeorm'; import { EVENT_NAME } from './event-logger.interface'; import { EventLoggerService } from './event-logger.service'; @@ -10,11 +15,14 @@ import { EventLoggerService } from './event-logger.service'; describe('EventLoggerService', () => { let service: EventLoggerService; let mockEventLoggerRepository: DeepMocked>; + let mockCrispService: DeepMocked; beforeEach(async () => { mockEventLoggerRepository = createMock>( mockEventLoggerRepositoryMethods, ); + const mockedUserRepository = createMock>(mockUserRepositoryMethods); + mockCrispService = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -23,6 +31,11 @@ describe('EventLoggerService', () => { provide: getRepositoryToken(EventLogEntity), useValue: mockEventLoggerRepository, }, + { + provide: getRepositoryToken(UserEntity), + useValue: mockedUserRepository, + }, + { provide: CrispService, useValue: mockCrispService }, ], }).compile(); diff --git a/src/event-logger/event-logger.service.ts b/src/event-logger/event-logger.service.ts index c6b1cbfe..79f59d5f 100644 --- a/src/event-logger/event-logger.service.ts +++ b/src/event-logger/event-logger.service.ts @@ -1,29 +1,56 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { EventLogEntity } from 'src/entities/event-log.entity'; +import { UserEntity } from 'src/entities/user.entity'; import { Repository } from 'typeorm'; import { ICreateEventLog } from './event-logger.interface'; +const logger = new Logger('EventLogger'); + @Injectable() export class EventLoggerService { constructor( @InjectRepository(EventLogEntity) private eventLoggerRepository: Repository, + @InjectRepository(UserEntity) + private userRepository: Repository, ) {} async getEventLog(id: string): Promise { return await this.eventLoggerRepository.findOneBy({ id }); } - async createEventLog({ userId, event, date }: ICreateEventLog) { + async createEventLog({ email, userId, event, date }: ICreateEventLog) { try { + if (!userId && !email) { + logger.error('createEventLog - failed to create event log - no user id or email provided'); + throw new HttpException( + `createEventLog - failed to create event log - no user id or email provided`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + + if (!userId && email) { + const user = await this.userRepository.findOneBy({ email }); + if (!user) { + logger.error( + 'createEventLog - failed to create event log - no user found for email provided', + ); + throw new HttpException( + `createEventLog - failed to create event log - no user found for email provided`, + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + userId = user.id; + } + const eventLog = await this.eventLoggerRepository.create({ userId, event, date, }); - const savedEventLog = this.eventLoggerRepository.save(eventLog); - return savedEventLog; + const eventLogRecord = this.eventLoggerRepository.save(eventLog); + return eventLogRecord; } catch (err) { throw new HttpException( `createEventLog - failed to create event log ${err}`, diff --git a/src/firebase/firebase.module.ts b/src/firebase/firebase.module.ts index 2fe7ecf2..7fe7481e 100644 --- a/src/firebase/firebase.module.ts +++ b/src/firebase/firebase.module.ts @@ -3,7 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { ZapierWebhookClient } from 'src/api/zapier/zapier-webhook-client'; import { CourseUserService } from 'src/course-user/course-user.service'; +import { CrispService } from 'src/crisp/crisp.service'; import { CourseUserEntity } from 'src/entities/course-user.entity'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerAdminEntity } from 'src/entities/partner-admin.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; @@ -11,6 +13,7 @@ import { SubscriptionUserEntity } from 'src/entities/subscription-user.entity'; import { SubscriptionEntity } from 'src/entities/subscription.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { PartnerService } from 'src/partner/partner.service'; import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; import { SubscriptionUserService } from 'src/subscription-user/subscription-user.service'; @@ -31,6 +34,7 @@ import { FIREBASE, firebaseFactory } from './firebase-factory'; SubscriptionUserEntity, SubscriptionEntity, TherapySessionEntity, + EventLogEntity, ]), ], providers: [ @@ -45,6 +49,8 @@ import { FIREBASE, firebaseFactory } from './firebase-factory'; TherapySessionService, ZapierWebhookClient, SlackMessageClient, + CrispService, + EventLoggerService, ], exports: [FIREBASE], }) diff --git a/src/partner-access/partner-access.module.ts b/src/partner-access/partner-access.module.ts index 12968efb..e35fcb87 100644 --- a/src/partner-access/partner-access.module.ts +++ b/src/partner-access/partner-access.module.ts @@ -3,7 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { ZapierWebhookClient } from 'src/api/zapier/zapier-webhook-client'; import { CourseUserService } from 'src/course-user/course-user.service'; +import { CrispService } from 'src/crisp/crisp.service'; import { CourseUserEntity } from 'src/entities/course-user.entity'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerAdminEntity } from 'src/entities/partner-admin.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; @@ -11,6 +13,7 @@ import { SubscriptionUserEntity } from 'src/entities/subscription-user.entity'; import { SubscriptionEntity } from 'src/entities/subscription.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { PartnerService } from 'src/partner/partner.service'; import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; import { SubscriptionUserService } from 'src/subscription-user/subscription-user.service'; @@ -33,6 +36,7 @@ import { PartnerAccessService } from './partner-access.service'; SubscriptionUserEntity, TherapySessionEntity, SubscriptionEntity, + EventLogEntity, ]), FirebaseModule, ], @@ -47,6 +51,8 @@ import { PartnerAccessService } from './partner-access.service'; SubscriptionUserService, SubscriptionService, TherapySessionService, + CrispService, + EventLoggerService, ZapierWebhookClient, SlackMessageClient, ], diff --git a/src/partner-access/partner-access.service.spec.ts b/src/partner-access/partner-access.service.spec.ts index e775c7b0..6d36c082 100644 --- a/src/partner-access/partner-access.service.spec.ts +++ b/src/partner-access/partner-access.service.spec.ts @@ -2,9 +2,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { sub } from 'date-fns'; -import * as crispApi from 'src/api/crisp/crisp-api'; import * as mailchimpApi from 'src/api/mailchimp/mailchimp-api'; +import { CrispService } from 'src/crisp/crisp.service'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; import { GetUserDto } from 'src/user/dtos/get-user.dto'; import { @@ -43,17 +45,12 @@ const mockGetUserDto = { therapySessions: [], } as GetUserDto; -jest.mock('src/api/crisp/crisp-api', () => ({ - getCrispProfileData: jest.fn(), - updateCrispProfileBase: jest.fn(), - updateCrispProfile: jest.fn(), -})); - jest.mock('src/api/mailchimp/mailchimp-api', () => ({ createMailchimpMergeField: jest.fn(), createMailchimpProfile: jest.fn(), updateMailchimpProfile: jest.fn(), })); +const mockCrispServiceMethods = {}; describe('PartnerAccessService', () => { let service: PartnerAccessService; @@ -61,6 +58,9 @@ describe('PartnerAccessService', () => { let mockPartnerRepository: DeepMocked>; let mockPartnerAccessRepository: DeepMocked>; let mockServiceUserProfilesService: DeepMocked; + let mockCrispService: DeepMocked; + let mockEventLoggerService: DeepMocked; + let mockEventLogRepository: DeepMocked>; beforeEach(async () => { jest.clearAllMocks(); @@ -70,6 +70,9 @@ describe('PartnerAccessService', () => { mockPartnerAccessRepositoryMethods, ); mockServiceUserProfilesService = createMock(); + mockCrispService = createMock(mockCrispServiceMethods); + mockEventLoggerService = createMock(); + mockEventLogRepository = createMock>(mockEventLogRepository); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -82,7 +85,13 @@ describe('PartnerAccessService', () => { provide: getRepositoryToken(PartnerEntity), useValue: mockPartnerRepository, }, + { + provide: getRepositoryToken(EventLogEntity), + useValue: mockEventLogRepository, + }, { provide: ServiceUserProfilesService, useValue: mockServiceUserProfilesService }, + { provide: CrispService, useValue: mockCrispService }, + { provide: EventLoggerService, useValue: mockEventLoggerService }, ], }).compile(); @@ -166,7 +175,7 @@ describe('PartnerAccessService', () => { // Mocks that the accesscode already exists jest.spyOn(repo, 'findOne').mockResolvedValueOnce(mockPartnerAccessEntity); - jest.spyOn(crispApi, 'updateCrispProfile').mockImplementationOnce(async () => { + jest.spyOn(mockCrispService, 'updateCrispPeopleData').mockImplementationOnce(async () => { throw new Error('Test throw'); }); diff --git a/src/service-user-profiles/service-user-profiles.module.ts b/src/service-user-profiles/service-user-profiles.module.ts index e82ab563..a4d1bd39 100644 --- a/src/service-user-profiles/service-user-profiles.module.ts +++ b/src/service-user-profiles/service-user-profiles.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { CrispService } from 'src/crisp/crisp.service'; import { UserEntity } from 'src/entities/user.entity'; import { UserService } from 'src/user/user.service'; import { ServiceUserProfilesService } from './service-user-profiles.service'; @Module({ imports: [TypeOrmModule.forFeature([UserEntity])], - providers: [ServiceUserProfilesService, UserService], + providers: [ServiceUserProfilesService, UserService, CrispService], }) export class ServiceUserProfilesModule {} diff --git a/src/service-user-profiles/service-user-profiles.service.spec.ts b/src/service-user-profiles/service-user-profiles.service.spec.ts index da11c049..2fb502a8 100644 --- a/src/service-user-profiles/service-user-profiles.service.spec.ts +++ b/src/service-user-profiles/service-user-profiles.service.spec.ts @@ -1,16 +1,12 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { - createCrispProfile, - updateCrispProfile, - updateCrispProfileBase, -} from 'src/api/crisp/crisp-api'; import { createMailchimpMergeField, createMailchimpProfile, updateMailchimpProfile, } from 'src/api/mailchimp/mailchimp-api'; +import { CrispService } from 'src/crisp/crisp.service'; import { UserEntity } from 'src/entities/user.entity'; import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; import { @@ -28,15 +24,17 @@ import { mailchimpMarketingPermissionId, } from '../utils/constants'; -jest.mock('src/api/crisp/crisp-api'); jest.mock('src/api/mailchimp/mailchimp-api'); +const mockCrispServiceMethods = {}; describe('Service user profiles', () => { let service: ServiceUserProfilesService; const mockedUserRepository = createMock>(mockUserRepositoryMethods); + const mockCrispService = createMock(mockCrispServiceMethods); beforeEach(async () => { jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ providers: [ ServiceUserProfilesService, @@ -44,6 +42,7 @@ describe('Service user profiles', () => { provide: getRepositoryToken(UserEntity), useValue: mockedUserRepository, }, + { provide: CrispService, useValue: mockCrispService }, ], }).compile(); @@ -58,7 +57,7 @@ describe('Service user profiles', () => { it('should create crisp and mailchimp profiles for a public user', async () => { await service.createServiceUserProfiles(mockUserEntity); - expect(createCrispProfile).toHaveBeenCalledWith({ + expect(mockCrispService.createCrispProfile).toHaveBeenCalledWith({ email: mockUserEntity.email, person: { nickname: mockUserEntity.name, locales: [mockUserEntity.signUpLanguage] }, segments: ['public'], @@ -67,7 +66,7 @@ describe('Service user profiles', () => { const createdAt = mockUserEntity.createdAt.toISOString(); const lastActiveAt = mockUserEntity.lastActiveAt.toISOString(); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { marketing_permission: mockUserEntity.contactPermission, service_emails_permission: mockUserEntity.serviceEmailsPermission, @@ -119,13 +118,13 @@ describe('Service user profiles', () => { const createdAt = mockUserEntity.createdAt.toISOString(); const lastActiveAt = mockUserEntity.lastActiveAt.toISOString(); - expect(createCrispProfile).toHaveBeenCalledWith({ + expect(mockCrispService.createCrispProfile).toHaveBeenCalledWith({ email: mockUserEntity.email, person: { nickname: mockUserEntity.name, locales: [mockUserEntity.signUpLanguage] }, segments: [partnerName], }); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { signed_up_at: createdAt, marketing_permission: mockUserEntity.contactPermission, @@ -167,7 +166,7 @@ describe('Service user profiles', () => { }); it('should not propagate external api call errors', async () => { - const mocked = jest.mocked(createCrispProfile); + const mocked = jest.mocked(mockCrispService.createCrispProfile); mocked.mockRejectedValue(new Error('Crisp API call failed')); await expect(service.createServiceUserProfiles(mockUserEntity)).resolves.not.toThrow(); mocked.mockReset(); @@ -185,8 +184,8 @@ describe('Service user profiles', () => { const lastActiveAt = mockUserEntity.lastActiveAt.toISOString(); - expect(updateCrispProfile).toHaveBeenCalledTimes(1); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledTimes(1); + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { marketing_permission: mockUserEntity.contactPermission, service_emails_permission: mockUserEntity.serviceEmailsPermission, @@ -228,7 +227,7 @@ describe('Service user profiles', () => { await service.updateServiceUserProfilesUser(mockUser, false, false, mockUser.email); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { marketing_permission: false, service_emails_permission: false, @@ -267,10 +266,10 @@ describe('Service user profiles', () => { mockUserEntity.email, ); - expect(updateCrispProfile).toHaveBeenCalled(); + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalled(); expect(updateMailchimpProfile).toHaveBeenCalled(); - expect(updateCrispProfileBase).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispProfileBase).toHaveBeenCalledWith( { person: { nickname: mockUserEntity.name, @@ -291,12 +290,12 @@ describe('Service user profiles', () => { oldEmail, ); const serialisedMockUserData = service.serializeUserData(mockUserEntity); - expect(updateCrispProfileBase).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispProfileBase).toHaveBeenCalledWith( { email: newEmail, person: { locales: ['en'], nickname: 'name' } }, oldEmail, ); - expect(updateCrispProfile).toHaveBeenCalledTimes(1); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledTimes(1); + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { email_reminders_frequency: EMAIL_REMINDERS_FREQUENCY.TWO_MONTHS, last_active_at: mockUserEntity.lastActiveAt.toISOString(), @@ -330,14 +329,14 @@ describe('Service user profiles', () => { const partnerString = mockPartnerAccessEntity.partner.name.toLowerCase(); - expect(updateCrispProfileBase).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispProfileBase).toHaveBeenCalledWith( { segments: [partnerString], }, mockUserEntity.email, ); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { partners: partnerString, feature_live_chat: mockPartnerAccessEntity.featureLiveChat, @@ -368,14 +367,14 @@ describe('Service user profiles', () => { const partnerString = service.serializePartnersString(partnerAccesses); - expect(updateCrispProfileBase).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispProfileBase).toHaveBeenCalledWith( { segments: partnerString.split('; '), }, mockUserEntity.email, ); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { partners: partnerString, feature_live_chat: true, @@ -401,7 +400,7 @@ describe('Service user profiles', () => { }); it('should not propagate external api call errors', async () => { - const mocked = jest.mocked(updateCrispProfile); + const mocked = jest.mocked(mockCrispService.updateCrispPeopleData); mocked.mockRejectedValue(new Error('Crisp API call failed')); await expect( service.updateServiceUserProfilesPartnerAccess( @@ -431,7 +430,7 @@ describe('Service user profiles', () => { const nextTherapySessionAt = therapySession.startDateTime.toISOString(); const lastTherapySessionAt = ''; - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { therapy_sessions_remaining: 5, therapy_sessions_redeemed: 1, @@ -468,7 +467,7 @@ describe('Service user profiles', () => { const lastTherapySessionAt = mockAltPartnerAccessEntity.therapySession[0].startDateTime.toISOString(); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { therapy_sessions_remaining: 9, therapy_sessions_redeemed: 3, @@ -505,7 +504,7 @@ describe('Service user profiles', () => { const lastTherapySessionAt = mockAltPartnerAccessEntity.therapySession[0].startDateTime.toISOString(); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { therapy_sessions_remaining: 9, therapy_sessions_redeemed: 3, @@ -542,7 +541,7 @@ describe('Service user profiles', () => { const lastTherapySessionAt = mockAltPartnerAccessEntity.therapySession[0].startDateTime.toISOString(); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { therapy_sessions_remaining: 9, therapy_sessions_redeemed: 3, @@ -584,7 +583,7 @@ describe('Service user profiles', () => { it('should update crisp and mailchimp profile course data', async () => { await service.updateServiceUserProfilesCourse(mockCourseUserEntity, mockUserEntity.email); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { course_cn: 'Started', course_cn_sessions: 'WAB:C', @@ -604,7 +603,7 @@ describe('Service user profiles', () => { }); it('should not propagate external api call errors', async () => { - const mocked = jest.mocked(updateCrispProfile); + const mocked = jest.mocked(mockCrispService.updateCrispPeopleData); mocked.mockRejectedValue(new Error('Crisp API call failed')); await expect( service.updateServiceUserProfilesCourse(mockCourseUserEntity, mockUserEntity.email), diff --git a/src/service-user-profiles/service-user-profiles.service.ts b/src/service-user-profiles/service-user-profiles.service.ts index 50d48789..ccc4d49c 100644 --- a/src/service-user-profiles/service-user-profiles.service.ts +++ b/src/service-user-profiles/service-user-profiles.service.ts @@ -1,10 +1,5 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { - createCrispProfile, - updateCrispProfile, - updateCrispProfileBase, -} from 'src/api/crisp/crisp-api'; import { batchCreateMailchimpProfiles, createMailchimpMergeField, @@ -15,6 +10,7 @@ import { ListMemberPartial, MAILCHIMP_MERGE_FIELD_TYPES, } from 'src/api/mailchimp/mailchimp-api.interfaces'; +import { CrispService } from 'src/crisp/crisp.service'; import { CourseUserEntity } from 'src/entities/course-user.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; @@ -37,7 +33,10 @@ const logger = new Logger('ServiceUserProfiles'); @Injectable() export class ServiceUserProfilesService { - constructor(@InjectRepository(UserEntity) private userRepository: Repository) {} + constructor( + @InjectRepository(UserEntity) private userRepository: Repository, + private crispService: CrispService, + ) {} async createServiceUserProfiles( user: UserEntity, @@ -52,7 +51,7 @@ export class ServiceUserProfilesService { partnerAccess ? [{ ...partnerAccess, partner }] : [], ); - await createCrispProfile({ + await this.crispService.createCrispProfile({ email: email, person: { nickname: user.name, locales: [user.signUpLanguage || 'en'] }, segments: this.serializeCrispPartnerSegments(partner ? [partner] : []), @@ -60,7 +59,7 @@ export class ServiceUserProfilesService { const userSignedUpAt = user.createdAt?.toISOString(); - await updateCrispProfile( + await this.crispService.updateCrispPeopleData( { signed_up_at: userSignedUpAt, ...userData.crispSchema, @@ -99,7 +98,7 @@ export class ServiceUserProfilesService { try { if (isCrispBaseUpdateRequired) { // Extra call required to update crisp "base" profile when name or sign up language is changed - await updateCrispProfileBase( + await this.crispService.updateCrispProfileBase( { ...(isEmailUpdateRequired && { email: email }), person: { @@ -112,7 +111,7 @@ export class ServiceUserProfilesService { } const userData = this.serializeUserData(user); - await updateCrispProfile(userData.crispSchema, email); + await this.crispService.updateCrispPeopleData(userData.crispSchema, email); await updateMailchimpProfile( { ...userData.mailchimpSchema, @@ -134,7 +133,7 @@ export class ServiceUserProfilesService { this.createCompleteMailchimpUserProfile(userWithRelations); logger.log(`Created and updated service user profiles user. Email: ${email}`); } - logger.error(`Update service user profiles user error - ${error}`); + logger.error(`Update service user profiles user error - ${JSON.stringify(error)}`); } } @@ -144,7 +143,7 @@ export class ServiceUserProfilesService { ) { try { const partners = partnerAccesses.map((pa) => pa.partner); - await updateCrispProfileBase( + await this.crispService.updateCrispProfileBase( { segments: this.serializeCrispPartnerSegments(partners), }, @@ -152,7 +151,7 @@ export class ServiceUserProfilesService { ); const partnerAccessData = this.serializePartnerAccessData(partnerAccesses); - await updateCrispProfile(partnerAccessData.crispSchema, email); + await this.crispService.updateCrispPeopleData(partnerAccessData.crispSchema, email); await updateMailchimpProfile(partnerAccessData.mailchimpSchema, email); } catch (error) { logger.error(`Update service user profiles partner access error - ${error}`); @@ -162,7 +161,7 @@ export class ServiceUserProfilesService { async updateServiceUserProfilesTherapy(partnerAccesses: PartnerAccessEntity[], email) { try { const therapyData = this.serializeTherapyData(partnerAccesses); - await updateCrispProfile(therapyData.crispSchema, email); + await this.crispService.updateCrispPeopleData(therapyData.crispSchema, email); await updateMailchimpProfile(therapyData.mailchimpSchema, email); } catch (error) { logger.error(`Update service user profiles therapy error - ${error}`); @@ -172,7 +171,7 @@ export class ServiceUserProfilesService { async updateServiceUserProfilesCourse(courseUser: CourseUserEntity, email: string) { try { const courseData = this.serializeCourseData(courseUser); - await updateCrispProfile(courseData.crispSchema, email); + await this.crispService.updateCrispPeopleData(courseData.crispSchema, email); await updateMailchimpProfile(courseData.mailchimpSchema, email); } catch (error) { logger.error(`Update service user profiles course error - ${error}`); diff --git a/src/session-feedback/session-feedback.module.ts b/src/session-feedback/session-feedback.module.ts index 6bc114be..4d62e51d 100644 --- a/src/session-feedback/session-feedback.module.ts +++ b/src/session-feedback/session-feedback.module.ts @@ -2,6 +2,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { ZapierWebhookClient } from 'src/api/zapier/zapier-webhook-client'; +import { CrispService } from 'src/crisp/crisp.service'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; import { SessionFeedbackEntity } from 'src/entities/session-feedback.entity'; @@ -10,6 +12,7 @@ import { SubscriptionUserEntity } from 'src/entities/subscription-user.entity'; import { SubscriptionEntity } from 'src/entities/subscription.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; 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 { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; import { SessionService } from 'src/session/session.service'; @@ -31,6 +34,7 @@ import { SessionFeedbackService } from './session-feedback.service'; SubscriptionUserEntity, SubscriptionEntity, TherapySessionEntity, + EventLogEntity, ]), ], controllers: [SessionFeedbackController], @@ -43,6 +47,8 @@ import { SessionFeedbackService } from './session-feedback.service'; SubscriptionService, PartnerAccessService, TherapySessionService, + CrispService, + EventLoggerService, ZapierWebhookClient, SlackMessageClient, ], diff --git a/src/session-feedback/session-feedback.service.spec.ts b/src/session-feedback/session-feedback.service.spec.ts index 634052b9..2852c8ea 100644 --- a/src/session-feedback/session-feedback.service.spec.ts +++ b/src/session-feedback/session-feedback.service.spec.ts @@ -3,6 +3,8 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; +import { CrispService } from 'src/crisp/crisp.service'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; import { SessionFeedbackEntity } from 'src/entities/session-feedback.entity'; @@ -11,6 +13,7 @@ import { SubscriptionUserEntity } from 'src/entities/subscription-user.entity'; import { SubscriptionEntity } from 'src/entities/subscription.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { SessionFeedbackService } from 'src/session-feedback/session-feedback.service'; import { SessionService } from 'src/session/session.service'; import { FEEDBACK_TAGS_ENUM } from 'src/utils/constants'; @@ -36,6 +39,9 @@ describe('SessionFeedbackService', () => { let mockSessionFeedbackRepository: DeepMocked>; let mockSessionService: DeepMocked; let mockSlackMessageClient: DeepMocked; + let mockCrispService: DeepMocked; + let mockEventLoggerService: DeepMocked; + let mockEventLogRepository: DeepMocked>; beforeEach(async () => { mockPartnerAccessRepository = createMock>(); @@ -48,6 +54,9 @@ describe('SessionFeedbackService', () => { mockSessionFeedbackRepository = createMock>(); mockSessionService = createMock(); mockSlackMessageClient = createMock(); + mockCrispService = createMock(); + mockEventLoggerService = createMock(); + mockEventLogRepository = createMock>(mockEventLogRepository); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -89,6 +98,12 @@ describe('SessionFeedbackService', () => { useValue: mockSessionService, }, { provide: SlackMessageClient, useValue: mockSlackMessageClient }, + { + provide: getRepositoryToken(EventLogEntity), + useValue: mockEventLogRepository, + }, + { provide: CrispService, useValue: mockCrispService }, + { provide: EventLoggerService, useValue: mockEventLoggerService }, ], }).compile(); diff --git a/src/session-user/session-user.module.ts b/src/session-user/session-user.module.ts index 7d5f8d62..709c6741 100644 --- a/src/session-user/session-user.module.ts +++ b/src/session-user/session-user.module.ts @@ -2,8 +2,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { ZapierWebhookClient } from 'src/api/zapier/zapier-webhook-client'; +import { CrispService } from 'src/crisp/crisp.service'; import { CourseUserEntity } from 'src/entities/course-user.entity'; import { CourseEntity } from 'src/entities/course.entity'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerAdminEntity } from 'src/entities/partner-admin.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; @@ -13,6 +15,7 @@ import { SubscriptionUserEntity } from 'src/entities/subscription-user.entity'; import { SubscriptionEntity } from 'src/entities/subscription.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { PartnerService } from 'src/partner/partner.service'; import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; import { SubscriptionUserService } from 'src/subscription-user/subscription-user.service'; @@ -40,6 +43,7 @@ import { SessionUserService } from './session-user.service'; SubscriptionUserEntity, TherapySessionEntity, SubscriptionEntity, + EventLogEntity, ]), ], controllers: [SessionUserController], @@ -55,6 +59,8 @@ import { SessionUserService } from './session-user.service'; SubscriptionUserService, TherapySessionService, SubscriptionService, + CrispService, + EventLoggerService, ZapierWebhookClient, SlackMessageClient, ], diff --git a/src/subscription-user/subscription-user.module.ts b/src/subscription-user/subscription-user.module.ts index 40590be3..dba19832 100644 --- a/src/subscription-user/subscription-user.module.ts +++ b/src/subscription-user/subscription-user.module.ts @@ -1,6 +1,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; +import { CrispService } from 'src/crisp/crisp.service'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerAdminEntity } from 'src/entities/partner-admin.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; @@ -8,6 +10,7 @@ import { SubscriptionUserEntity } from 'src/entities/subscription-user.entity'; import { SubscriptionEntity } from 'src/entities/subscription.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { PartnerService } from 'src/partner/partner.service'; import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; import { TherapySessionService } from 'src/therapy-session/therapy-session.service'; @@ -29,6 +32,7 @@ import { SubscriptionUserService } from './subscription-user.service'; PartnerEntity, PartnerAdminEntity, TherapySessionEntity, + EventLogEntity, ]), FirebaseModule, ], @@ -40,6 +44,8 @@ import { SubscriptionUserService } from './subscription-user.service'; PartnerAccessService, ServiceUserProfilesService, ZapierWebhookClient, + CrispService, + EventLoggerService, PartnerService, TherapySessionService, SlackMessageClient, diff --git a/src/subscription-user/subscription-user.service.spec.ts b/src/subscription-user/subscription-user.service.spec.ts index 30976f40..83733386 100644 --- a/src/subscription-user/subscription-user.service.spec.ts +++ b/src/subscription-user/subscription-user.service.spec.ts @@ -2,7 +2,10 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest/lib/mocks'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { ZapierWebhookClient } from 'src/api/zapier/zapier-webhook-client'; +import { CrispService } from 'src/crisp/crisp.service'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { SubscriptionUserEntity } from 'src/entities/subscription-user.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { SubscriptionService } from 'src/subscription/subscription.service'; import { mockSubscriptionUserEntity, mockUserEntity } from 'test/utils/mockData'; import { @@ -15,15 +18,20 @@ import { SubscriptionUserService } from './subscription-user.service'; describe('SubscriptionUserService', () => { let service: SubscriptionUserService; let mockedSubscriptionUserRepository: DeepMocked>; - let mockSubscriptionService: DeepMocked; const mockedZapierWebhookClient = createMock(mockZapierWebhookClientMethods); + let mockCrispService: DeepMocked; + let mockEventLoggerService: DeepMocked; + let mockEventLogRepository: DeepMocked>; beforeEach(async () => { mockedSubscriptionUserRepository = createMock>( mockSubscriptionUserRepositoryMethods, ); mockSubscriptionService = createMock(mockSubscriptionService); + mockCrispService = createMock(); + mockEventLoggerService = createMock(); + mockEventLogRepository = createMock>(mockEventLogRepository); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -40,6 +48,12 @@ describe('SubscriptionUserService', () => { provide: ZapierWebhookClient, useValue: mockedZapierWebhookClient, }, + { + provide: getRepositoryToken(EventLogEntity), + useValue: mockEventLogRepository, + }, + { provide: CrispService, useValue: mockCrispService }, + { provide: EventLoggerService, useValue: mockEventLoggerService }, ], }).compile(); diff --git a/src/user/user.module.ts b/src/user/user.module.ts index 1f1e5ab9..0968a18a 100644 --- a/src/user/user.module.ts +++ b/src/user/user.module.ts @@ -2,6 +2,8 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { ZapierWebhookClient } from 'src/api/zapier/zapier-webhook-client'; +import { CrispService } from 'src/crisp/crisp.service'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerAdminEntity } from 'src/entities/partner-admin.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; @@ -9,6 +11,7 @@ import { SubscriptionUserEntity } from 'src/entities/subscription-user.entity'; import { SubscriptionEntity } from 'src/entities/subscription.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.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'; @@ -29,6 +32,7 @@ import { UserService } from './user.service'; PartnerAccessEntity, PartnerAdminEntity, TherapySessionEntity, + EventLogEntity, ]), FirebaseModule, ], @@ -41,6 +45,8 @@ import { UserService } from './user.service'; SubscriptionService, SubscriptionUserService, TherapySessionService, + CrispService, + EventLoggerService, ZapierWebhookClient, SlackMessageClient, ], diff --git a/src/user/user.service.spec.ts b/src/user/user.service.spec.ts index ed1479b6..63cce7b9 100644 --- a/src/user/user.service.spec.ts +++ b/src/user/user.service.spec.ts @@ -3,10 +3,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { createCrispProfile, updateCrispProfile } from 'src/api/crisp/crisp-api'; import { createMailchimpProfile, updateMailchimpProfile } from 'src/api/mailchimp/mailchimp-api'; +import { CrispService } from 'src/crisp/crisp.service'; +import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; +import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; import { SubscriptionUserService } from 'src/subscription-user/subscription-user.service'; import { TherapySessionService } from 'src/therapy-session/therapy-session.service'; @@ -55,8 +57,8 @@ const updateUserDto: Partial = { const mockSubscriptionUserServiceMethods = {}; const mockTherapySessionServiceMethods = {}; +const mockCrispServiceMethods = {}; -jest.mock('src/api/crisp/crisp-api'); jest.mock('src/api/mailchimp/mailchimp-api'); describe('UserService', () => { @@ -66,6 +68,9 @@ describe('UserService', () => { let mockPartnerAccessService: DeepMocked; let mockSubscriptionUserService: DeepMocked; let mockTherapySessionService: DeepMocked; + let mockCrispService: DeepMocked; + let mockEventLoggerService: DeepMocked; + let mockEventLogRepository: DeepMocked>; beforeEach(async () => { jest.clearAllMocks(); @@ -75,6 +80,9 @@ describe('UserService', () => { mockSubscriptionUserServiceMethods, ); mockTherapySessionService = createMock(mockTherapySessionServiceMethods); + mockCrispService = createMock(mockCrispServiceMethods); + mockEventLoggerService = createMock(); + mockEventLogRepository = createMock>(mockEventLogRepository); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -102,6 +110,12 @@ describe('UserService', () => { { provide: SubscriptionUserService, useValue: mockSubscriptionUserService }, { provide: TherapySessionService, useValue: mockTherapySessionService }, ServiceUserProfilesService, + { + provide: getRepositoryToken(EventLogEntity), + useValue: mockEventLogRepository, + }, + { provide: CrispService, useValue: mockCrispService }, + { provide: EventLoggerService, useValue: mockEventLoggerService }, ], }).compile(); @@ -129,12 +143,12 @@ describe('UserService', () => { expect(user.partnerAccesses).toBeNull(); // Test services user profiles are created - expect(createCrispProfile).toHaveBeenCalledWith({ + expect(mockCrispService.createCrispProfile).toHaveBeenCalledWith({ email: user.user.email, person: { nickname: user.user.name, locales: [user.user.signUpLanguage] }, segments: ['public'], }); - expect(updateCrispProfile).toHaveBeenCalled(); + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalled(); expect(createMailchimpProfile).toHaveBeenCalled(); }); @@ -162,12 +176,12 @@ describe('UserService', () => { ]); // Test services user profiles are created - expect(createCrispProfile).toHaveBeenCalledWith({ + expect(mockCrispService.createCrispProfile).toHaveBeenCalledWith({ email: user.user.email, person: { nickname: 'name', locales: ['en'] }, segments: ['bumble'], }); - expect(updateCrispProfile).toHaveBeenCalledWith( + expect(mockCrispService.updateCrispPeopleData).toHaveBeenCalledWith( { signed_up_at: user.user.createdAt, last_active_at: (user.user.lastActiveAt as Date).toISOString(), @@ -233,7 +247,7 @@ describe('UserService', () => { }); it('should not fail create on crisp api call errors', async () => { - const mocked = jest.mocked(createCrispProfile); + const mocked = jest.mocked(mockCrispService.createCrispProfile); mocked.mockRejectedValue(new Error('Crisp API call failed')); const user = await service.createUser(createUserDto); @@ -313,7 +327,7 @@ describe('UserService', () => { }); it('should not fail update on crisp api call errors', async () => { - const mocked = jest.mocked(updateCrispProfile); + const mocked = jest.mocked(mockCrispService.updateCrispPeopleData); mocked.mockRejectedValue(new Error('Crisp API call failed')); const user = await service.updateUser(updateUserDto, mockUserEntity.id); diff --git a/src/user/user.service.ts b/src/user/user.service.ts index e5ce9916..41e184ff 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -1,6 +1,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { deleteMailchimpProfile } from 'src/api/mailchimp/mailchimp-api'; +import { CrispService } from 'src/crisp/crisp.service'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; import { UserEntity } from 'src/entities/user.entity'; @@ -13,7 +14,6 @@ import { SIGNUP_TYPE } from 'src/utils/constants'; import { FIREBASE_ERRORS } from 'src/utils/errors'; import { FIREBASE_EVENTS, USER_SERVICE_EVENTS } from 'src/utils/logs'; import { ILike, IsNull, Not, Repository } from 'typeorm'; -import { deleteCrispProfile, deleteCypressCrispProfiles } from '../api/crisp/crisp-api'; import { AuthService } from '../auth/auth.service'; import { basePartnerAccess, PartnerAccessService } from '../partner-access/partner-access.service'; import { formatUserObject } from '../utils/serialize'; @@ -39,6 +39,7 @@ export class UserService { private readonly therapySessionService: TherapySessionService, private readonly partnerAccessService: PartnerAccessService, private readonly serviceUserProfilesService: ServiceUserProfilesService, + private readonly crispService: CrispService, ) {} public async createUser(createUserDto: CreateUserDto): Promise { @@ -300,7 +301,7 @@ export class UserService { await Promise.all( batch.map(async (user) => { try { - await deleteCrispProfile(user.email); + await this.crispService.deleteCrispProfile(user.email); } catch (error) { this.logger.warn( `deleteCypressTestUsers - unable to delete crisp profile for user ${user.id}`, @@ -366,7 +367,7 @@ export class UserService { await this.authService.deleteCypressFirebaseUsers(); // Delete all remaining crisp accounts - await deleteCypressCrispProfiles(); + await this.crispService.deleteCypressCrispProfiles(); } } catch (error) { // If this fails we don't want to break cypress tests but we want to be alerted diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 38071726..d77e8ef5 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -129,6 +129,8 @@ export const firebaseMeasurementId = getEnv( export const zapierToken = getEnv(process.env.ZAPIER_TOKEN, 'ZAPIER_TOKEN'); +export const crispPluginId = getEnv(process.env.CRISP_PLUGIN_ID, 'CRISP_PLUGIN_ID'); +export const crispPluginKey = getEnv(process.env.CRISP_PLUGIN_KEY, 'CRISP_PLUGIN_KEY'); export const crispToken = getEnv(process.env.CRISP_TOKEN, 'CRISP_TOKEN'); export const crispWebsiteId = getEnv(process.env.CRISP_WEBSITE_ID, 'CRISP_WEBSITE_ID'); diff --git a/src/webhooks/webhooks.controller.ts b/src/webhooks/webhooks.controller.ts index 730010dd..5c3162d2 100644 --- a/src/webhooks/webhooks.controller.ts +++ b/src/webhooks/webhooks.controller.ts @@ -11,11 +11,9 @@ import { } from '@nestjs/common'; import { ApiBody, ApiTags } from '@nestjs/swagger'; import { createHmac } from 'crypto'; -import { EventLogEntity } from 'src/entities/event-log.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; import { storyblokWebhookSecret } from 'src/utils/constants'; import { ControllerDecorator } from 'src/utils/controller.decorator'; -import { WebhookCreateEventLogDto } from 'src/webhooks/dto/webhook-create-event-log.dto'; import { ZapierSimplybookBodyDto } from '../partner-access/dtos/zapier-body.dto'; import { ZapierAuthGuard } from '../partner-access/zapier-auth.guard'; import { StoryDto } from './dto/story.dto'; @@ -37,13 +35,6 @@ export class WebhooksController { return this.webhooksService.updatePartnerAccessTherapy(simplybookBodyDto); } - @UseGuards(ZapierAuthGuard) - @Post('event-log') - @ApiBody({ type: WebhookCreateEventLogDto }) - async createEventLog(@Body() createEventLogDto): Promise { - return this.webhooksService.createEventLog(createEventLogDto); - } - @Post('storyblok') @ApiBody({ type: StoryDto }) async updateStory(@Request() req, @Body() data: StoryDto, @Headers() headers) { diff --git a/src/webhooks/webhooks.module.ts b/src/webhooks/webhooks.module.ts index f5973cb8..1e939312 100644 --- a/src/webhooks/webhooks.module.ts +++ b/src/webhooks/webhooks.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { CoursePartnerService } from 'src/course-partner/course-partner.service'; +import { CrispService } from 'src/crisp/crisp.service'; import { CoursePartnerEntity } from 'src/entities/course-partner.entity'; import { CourseEntity } from 'src/entities/course.entity'; import { EventLogEntity } from 'src/entities/event-log.entity'; @@ -37,6 +38,7 @@ import { WebhooksService } from './webhooks.service'; PartnerService, ServiceUserProfilesService, SlackMessageClient, + CrispService, EventLoggerService, ], controllers: [WebhooksController], diff --git a/src/webhooks/webhooks.service.spec.ts b/src/webhooks/webhooks.service.spec.ts index 9541588e..5513ab02 100644 --- a/src/webhooks/webhooks.service.spec.ts +++ b/src/webhooks/webhooks.service.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { CoursePartnerService } from 'src/course-partner/course-partner.service'; +import { CrispService } from 'src/crisp/crisp.service'; import { CoursePartnerEntity } from 'src/entities/course-partner.entity'; import { CourseEntity } from 'src/entities/course.entity'; import { EventLogEntity } from 'src/entities/event-log.entity'; @@ -12,7 +13,6 @@ import { PartnerEntity } from 'src/entities/partner.entity'; import { SessionEntity } from 'src/entities/session.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { EVENT_NAME } from 'src/event-logger/event-logger.interface'; import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { PartnerService } from 'src/partner/partner.service'; import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; @@ -33,7 +33,6 @@ import { mockCoursePartnerServiceMethods, mockCourseRepositoryMethods, mockEventLoggerRepositoryMethods, - mockEventLoggerServiceMethods, mockPartnerAccessRepositoryMethods, mockPartnerAdminRepositoryMethods, mockPartnerRepositoryMethods, @@ -43,7 +42,6 @@ import { mockUserRepositoryMethods, } from 'test/utils/mockedServices'; import { ILike, Repository } from 'typeorm'; -import { WebhookCreateEventLogDto } from './dto/webhook-create-event-log.dto'; import { WebhooksService } from './webhooks.service'; // Difficult to mock classes as well as node modules. @@ -70,7 +68,6 @@ jest.mock('src/api/simplybook/simplybook-api', () => { }, }; }); -jest.mock('src/api/crisp/crisp-api'); describe('WebhooksService', () => { let service: WebhooksService; @@ -89,10 +86,6 @@ describe('WebhooksService', () => { const mockedPartnerAccessRepository = createMock>( mockPartnerAccessRepositoryMethods, ); - const mockedEventLoggerService = createMock(mockEventLoggerServiceMethods); - const mockedEventLogRepository = createMock>( - mockEventLoggerRepositoryMethods, - ); const mockedPartnerRepository = createMock>( mockPartnerRepositoryMethods, ); @@ -103,6 +96,11 @@ describe('WebhooksService', () => { mockPartnerAdminRepositoryMethods, ); const mockedServiceUserProfilesService = createMock(); + const mockCrispService = createMock(); + const mockEventLoggerService = createMock(); + const mockEventLogRepository = createMock>( + mockEventLoggerRepositoryMethods, + ); beforeEach(async () => { jest.clearAllMocks(); @@ -141,10 +139,6 @@ describe('WebhooksService', () => { provide: getRepositoryToken(PartnerAdminEntity), useValue: mockedPartnerAdminRepository, }, - { - provide: getRepositoryToken(EventLogEntity), - useValue: mockedEventLogRepository, - }, { provide: ServiceUserProfilesService, useValue: mockedServiceUserProfilesService, @@ -153,12 +147,17 @@ describe('WebhooksService', () => { provide: CoursePartnerService, useValue: mockedCoursePartnerService, }, - { provide: EventLoggerService, useValue: mockedEventLoggerService }, { provide: SlackMessageClient, useValue: mockedSlackMessageClient, }, PartnerService, + { + provide: getRepositoryToken(EventLogEntity), + useValue: mockEventLogRepository, + }, + { provide: CrispService, useValue: mockCrispService }, + { provide: EventLoggerService, useValue: mockEventLoggerService }, ], }).compile(); @@ -693,50 +692,4 @@ describe('WebhooksService', () => { expect(therapySessionFindOneSpy).toHaveBeenCalled(); }); }); - - describe('createEventLog', () => { - it('should create an eventLog if DTO is correct', async () => { - const eventDto: WebhookCreateEventLogDto = { - event: EVENT_NAME.CHAT_MESSAGE_SENT, - date: new Date(2000, 1, 1), - email: 'a@b.com', - }; - const log = await service.createEventLog(eventDto); - - expect(log).toEqual({ - date: new Date(2000, 1, 1), - event: 'CHAT_MESSAGE_SENT', - id: 'eventLogId1ß', - userId: 'userId1', - }); - }); - it('should throw 404 if email is not related to a user is incorrect', async () => { - const eventDto: WebhookCreateEventLogDto = { - event: EVENT_NAME.CHAT_MESSAGE_SENT, - date: new Date(2000, 1, 1), - email: 'a@b.com', - }; - jest.spyOn(mockedUserRepository, 'findOneBy').mockImplementationOnce(async () => { - return null; - }); - - await expect(service.createEventLog(eventDto)).rejects.toThrow( - `createEventLog webhook failed - no user attached to email a@b.com`, - ); - }); - it('should throw 500 if failed to create user', async () => { - const eventDto: WebhookCreateEventLogDto = { - event: EVENT_NAME.CHAT_MESSAGE_SENT, - date: new Date(2000, 1, 1), - email: 'a@b.com', - }; - jest.spyOn(mockedEventLoggerService, 'createEventLog').mockImplementationOnce(async () => { - throw new Error('Unable to create event log error'); - }); - - await expect(service.createEventLog(eventDto)).rejects.toThrow( - `Unable to create event log error`, - ); - }); - }); }); diff --git a/src/webhooks/webhooks.service.ts b/src/webhooks/webhooks.service.ts index 34401428..8f0421b5 100644 --- a/src/webhooks/webhooks.service.ts +++ b/src/webhooks/webhooks.service.ts @@ -2,17 +2,14 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { CourseEntity } from 'src/entities/course.entity'; -import { EventLogEntity } from 'src/entities/event-log.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { SessionEntity } from 'src/entities/session.entity'; import { TherapySessionEntity } from 'src/entities/therapy-session.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { EventLoggerService } from 'src/event-logger/event-logger.service'; import { ZapierSimplybookBodyDto } from 'src/partner-access/dtos/zapier-body.dto'; import { ServiceUserProfilesService } from 'src/service-user-profiles/service-user-profiles.service'; import { IUser } from 'src/user/user.interface'; import { serializeZapierSimplyBookDtoToTherapySessionEntity } from 'src/utils/serialize'; -import { WebhookCreateEventLogDto } from 'src/webhooks/dto/webhook-create-event-log.dto'; import StoryblokClient from 'storyblok-js-client'; import { ILike, MoreThan, Repository } from 'typeorm'; import { CoursePartnerService } from '../course-partner/course-partner.service'; @@ -37,7 +34,6 @@ export class WebhooksService { private readonly coursePartnerService: CoursePartnerService, @InjectRepository(TherapySessionEntity) private therapySessionRepository: Repository, - private eventLoggerService: EventLoggerService, private serviceUserProfilesService: ServiceUserProfilesService, private slackMessageClient: SlackMessageClient, ) {} @@ -394,30 +390,4 @@ export class WebhooksService { } } } - - async createEventLog(createEventDto: WebhookCreateEventLogDto): Promise { - if (!createEventDto.email && !createEventDto.userId) { - const error = `createEventLog webhook failed - neither user email or userId was provided`; - this.logger.error(error); - throw new HttpException(error, HttpStatus.BAD_REQUEST); - } - - // Only fetch user object if the userId is not provided - const user = createEventDto.userId - ? undefined - : await this.userRepository.findOneBy({ email: ILike(createEventDto.email) }); - - if (user || createEventDto.userId) { - const event = await this.eventLoggerService.createEventLog({ - userId: createEventDto.userId || user.id, - event: createEventDto.event, - date: createEventDto.date, - }); - return event; - } else { - const error = `createEventLog webhook failed - no user attached to email ${createEventDto.email}`; - this.logger.error(error); - throw new HttpException(error, HttpStatus.NOT_FOUND); - } - } } diff --git a/yarn.lock b/yarn.lock index 9c08b29f..bec21be8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1473,6 +1473,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@sindresorhus/is@^4.0.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" + integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== + "@sinonjs/commons@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" @@ -1487,11 +1492,23 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@socket.io/component-emitter@~3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz#821f8442f4175d8f0467b9daf26e3a18e2d02af2" + integrity sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA== + "@sqltools/formatter@^1.2.5": version "1.2.5" resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12" integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw== +"@szmarczak/http-timer@^4.0.5": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" + integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== + dependencies: + defer-to-connect "^2.0.0" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -1558,6 +1575,16 @@ "@types/connect" "*" "@types/node" "*" +"@types/cacheable-request@^6.0.1": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" + integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "^3.1.4" + "@types/node" "*" + "@types/responselike" "^1.0.0" + "@types/caseless@*": version "0.12.5" resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" @@ -1644,6 +1671,11 @@ dependencies: "@types/node" "*" +"@types/http-cache-semantics@*": + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== + "@types/http-errors@*": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" @@ -1688,6 +1720,13 @@ dependencies: "@types/node" "*" +"@types/keyv@^3.1.4": + version "3.1.4" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" + integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== + dependencies: + "@types/node" "*" + "@types/lodash@^4.17.7": version "4.17.7" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.7.tgz#2f776bcb53adc9e13b2c0dfd493dfcbd7de43612" @@ -1752,6 +1791,13 @@ "@types/tough-cookie" "*" form-data "^2.5.0" +"@types/responselike@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50" + integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw== + dependencies: + "@types/node" "*" + "@types/send@*": version "0.17.4" resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" @@ -2244,7 +2290,7 @@ arrify@^2.0.0: resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== -asap@^2.0.0, asap@~2.0.6: +asap@^2.0.0, asap@~2.0.3, asap@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== @@ -2480,6 +2526,24 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cacheable-lookup@^5.0.3: + version "5.0.4" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" + integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== + +cacheable-request@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.4.tgz#7a33ebf08613178b403635be7b899d3e69bbe817" + integrity sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^6.0.1" + responselike "^2.0.0" + call-bind@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" @@ -2668,6 +2732,13 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +clone-response@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== + dependencies: + mimic-response "^1.0.0" + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" @@ -2848,6 +2919,22 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +crisp-api@^9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/crisp-api/-/crisp-api-9.2.0.tgz#03c695dc82e55ed2e9019a6d289c401771763ce6" + integrity sha512-lbet95fu/4BbeTRZl0xZEuGdgmn8C4RbF71NC7sWDdX0Kl6hgWZwgHAdw2eCvb2HIEExdlHmkm9JG6173uptMg== + dependencies: + fbemitter "github:crisp-dev/emitter#695f60594bdca0c876e5c232de57702ab3151b6f" + got "11.8.5" + socket.io-client "4.7.2" + +cross-fetch@^3.1.5: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -2893,7 +2980,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debug@^4.3.5: +debug@^4.3.5, debug@~4.3.1, debug@~4.3.2: version "4.3.7" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== @@ -2907,6 +2994,13 @@ decache@^3.0.5: dependencies: find "^0.2.4" +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + dedent@^1.0.0: version "1.5.3" resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" @@ -2929,6 +3023,11 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +defer-to-connect@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" + integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== + define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -3062,6 +3161,22 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" +engine.io-client@~6.5.2: + version "6.5.4" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.4.tgz#b8bc71ed3f25d0d51d587729262486b4b33bd0d0" + integrity sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + engine.io-parser "~5.2.1" + ws "~8.17.1" + xmlhttprequest-ssl "~2.0.0" + +engine.io-parser@~5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz#00dc5b97b1f233a23c9398d0209504cf5f94d92f" + integrity sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q== + enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0, enhanced-resolve@^5.7.0: version "5.16.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.1.tgz#e8bc63d51b826d6f1cbc0a150ecb5a8b0c62e567" @@ -3426,6 +3541,30 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +"fbemitter@github:crisp-dev/emitter#695f60594bdca0c876e5c232de57702ab3151b6f": + version "3.0.0" + resolved "https://codeload.github.com/crisp-dev/emitter/tar.gz/695f60594bdca0c876e5c232de57702ab3151b6f" + dependencies: + fbjs "3.0.4" + +fbjs-css-vars@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz#216551136ae02fe255932c3ec8775f18e2c078b8" + integrity sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ== + +fbjs@3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-3.0.4.tgz#e1871c6bd3083bac71ff2da868ad5067d37716c6" + integrity sha512-ucV0tDODnGV3JCnnkmoszb5lf4bNpzjv80K41wd4k798Etq+UYD0y0TIfalLjZoKgjive6/adkRnszwapiDgBQ== + dependencies: + cross-fetch "^3.1.5" + fbjs-css-vars "^1.0.0" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.30" + fecha@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" @@ -3744,6 +3883,13 @@ get-port@^3.1.0: resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc" integrity sha512-x5UJKlgeUiNT8nyo/AcnwLnZuZNcSjSw0kogRB+Whd1fjjFq4B1hySFxSFWWSn4mIBzg3sRNUDFYc4g5gjPoLg== +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + get-stream@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" @@ -3871,6 +4017,23 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +got@11.8.5: + version "11.8.5" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.5.tgz#ce77d045136de56e8f024bebb82ea349bc730046" + integrity sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ== + dependencies: + "@sindresorhus/is" "^4.0.0" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.2" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -3966,6 +4129,11 @@ http-basic@^8.1.1: http-response-object "^3.0.1" parse-cache-control "^1.0.1" +http-cache-semantics@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -3998,6 +4166,14 @@ http-response-object@^3.0.1: dependencies: "@types/node" "^10.0.3" +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" + integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -4686,7 +4862,7 @@ jose@^4.14.6: resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.5.tgz#6475d0f467ecd3c630a1b5dadd2735a7288df706" integrity sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -4851,7 +5027,7 @@ jws@^4.0.0: jwa "^2.0.0" safe-buffer "^5.0.1" -keyv@^4.5.4: +keyv@^4.0.0, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -5006,6 +5182,18 @@ long@^5.0.0, long@^5.2.3: resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + lru-cache@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -5148,6 +5336,16 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + minimalistic-assert@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" @@ -5340,7 +5538,7 @@ node-emoji@1.11.0: dependencies: lodash "^4.17.21" -node-fetch@^2.6.1, node-fetch@^2.6.9: +node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.9: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -5372,6 +5570,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + npm-run-path@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-3.1.0.tgz#7f91be317f6a466efed3c9f2980ad8a4ee8b0fa5" @@ -5386,7 +5589,7 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -5454,6 +5657,11 @@ os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== +p-cancelable@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" + integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== + p-limit@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" @@ -5755,6 +5963,13 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + promise@^8.0.0: version "8.3.0" resolved "https://registry.yarnpkg.com/promise/-/promise-8.3.0.tgz#8cb333d1edeb61ef23869fbb8a4ea0279ab60e0a" @@ -5863,6 +6078,11 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -5960,6 +6180,11 @@ require-in-the-middle@^7.4.0: module-details-from-path "^1.0.3" resolve "^1.22.8" +resolve-alpn@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" + integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -5991,6 +6216,13 @@ resolve@^1.1.6, resolve@^1.20.0, resolve@^1.22.8: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +responselike@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" + integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== + dependencies: + lowercase-keys "^2.0.0" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -6169,6 +6401,11 @@ set-function-length@^1.2.1: gopd "^1.0.1" has-property-descriptors "^1.0.2" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + setprototypeof@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" @@ -6233,6 +6470,24 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +socket.io-client@4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" + integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.2" + engine.io-client "~6.5.2" + socket.io-parser "~4.2.4" + +socket.io-parser@~4.2.4: + version "4.2.4" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.4.tgz#c806966cf7270601e47469ddeec30fbdfda44c83" + integrity sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew== + dependencies: + "@socket.io/component-emitter" "~3.1.0" + debug "~4.3.1" + source-map-support@0.5.13: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" @@ -6330,16 +6585,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6376,14 +6622,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6836,6 +7075,11 @@ typescript@^5.5.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== +ua-parser-js@^0.7.30: + version "0.7.39" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.39.tgz#c71efb46ebeabc461c4612d22d54f88880fabe7e" + integrity sha512-IZ6acm6RhQHNibSt7+c09hhvsKy9WUr4DVbeq9U8o71qxyYtJpQeDxQnMrVqnIFMLcQjHO0I9wgfO2vIahht4w== + uid@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.2.tgz#4b5782abf0f2feeefc00fa88006b2b3b7af3e3b9" @@ -7070,7 +7314,7 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -7088,15 +7332,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -7124,6 +7359,16 @@ ws@^8.17.1: resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== +ws@~8.17.1: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== + +xmlhttprequest-ssl@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" + integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== + xtend@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"