diff --git a/src/api/crisp/crisp-api.spec.ts b/src/api/crisp/crisp-api.spec.ts index 012b34d8..40e3f2c1 100644 --- a/src/api/crisp/crisp-api.spec.ts +++ b/src/api/crisp/crisp-api.spec.ts @@ -16,67 +16,36 @@ describe('CrispApi', () => { async () => ({ data: { error: false, reason: 'resolved', data: {} }, - } as AxiosResponse), + }) as AxiosResponse, ) .mockImplementationOnce( async () => ({ data: { error: false, reason: 'resolved', data: {} }, - } as AxiosResponse), + }) as AxiosResponse, ) .mockImplementationOnce( async () => ({ data: { error: false, reason: 'resolved', data: { segments: ['public'] } }, - } as AxiosResponse), + }) as AxiosResponse, ); // Clear the mock so the next test starts with fresh data - await updateCrispProfileAccesses( - mockUserEntity, - [{ ...mockPartnerAccessEntity, partner: mockPartnerEntity }], - [], - ); + await updateCrispProfileAccesses(mockUserEntity, [ + { ...mockPartnerAccessEntity, partner: mockPartnerEntity }, + ]); const baseUrl = `https://api.crisp.chat/v1/website/${crispWebsiteId}`; - // request to get profile data expect(apiCall).toHaveBeenNthCalledWith( 1, - expect.objectContaining({ - url: `${baseUrl}/people/data/${mockUserEntity.email}`, - type: 'get', - }), - ); - - expect(apiCall).toHaveBeenNthCalledWith( - 2, expect.objectContaining({ // updating people data url: `${baseUrl}/people/data/${mockUserEntity.email}`, type: 'patch', }), ); - expect(apiCall).toHaveBeenNthCalledWith( - 3, - // request to get - expect.objectContaining({ - // request to update profile - url: `${baseUrl}/people/profile/${mockUserEntity.email}`, - type: 'get', - }), - ); - expect(apiCall).toHaveBeenNthCalledWith( - 4, - // request to get - expect.objectContaining({ - // request to update profile - url: `${baseUrl}/people/profile/${mockUserEntity.email}`, - type: 'patch', - data: expect.objectContaining({ - segments: ['bumble', 'public'], - }), - }), - ); + expect(apiCall).toHaveBeenCalledTimes(1); mockedApiCall.mockClear(); }); }); diff --git a/src/api/crisp/crisp-api.ts b/src/api/crisp/crisp-api.ts index 1db684cc..062be6bf 100644 --- a/src/api/crisp/crisp-api.ts +++ b/src/api/crisp/crisp-api.ts @@ -25,6 +25,24 @@ const headers = { const logger = new Logger('UserService'); +export const updateCrispProfileTherapy = async (partnerAccesses: PartnerAccessEntity[], email) => { + const therapySessionsRemaining = partnerAccesses.reduce( + (sum, partnerAccess) => sum + partnerAccess.therapySessionsRemaining, + 0, + ); + const therapySessionsRedeemed = partnerAccesses.reduce( + (sum, partnerAccess) => sum + partnerAccess.therapySessionsRedeemed, + 0, + ); + + const therapyData = { + therapy_sessions_remaining: therapySessionsRemaining, + therapy_sessions_redeemed: therapySessionsRedeemed, + }; + + updateCrispProfileData(therapyData, email); +}; + export const updateCrispProfileAccesses = async ( user: UserEntity, partnerAccesses: PartnerAccessEntity[], diff --git a/src/partner-access/partner-access.service.spec.ts b/src/partner-access/partner-access.service.spec.ts index 1c2ee899..3466264b 100644 --- a/src/partner-access/partner-access.service.spec.ts +++ b/src/partner-access/partner-access.service.spec.ts @@ -130,42 +130,40 @@ describe('PartnerAccessService', () => { jest.spyOn(repo, 'save').mockImplementationOnce(async () => { return { ...mockPartnerAccessEntity, - id: '123456', + id: 'pa1', userId: mockGetUserDto.user.id, }; }); // Mocks that the accesscode already exists jest.spyOn(repo, 'findOne').mockResolvedValueOnce(mockPartnerAccessEntity); - const partnerAccess = await service.assignPartnerAccess(mockGetUserDto, '123456'); + const partnerAccess = await service.assignPartnerAccess(mockUserEntity, '123456'); expect(partnerAccess).toEqual({ ...mockPartnerAccessEntity, id: 'pa1', - userId: mockGetUserDto.user.id, + userId: mockUserEntity.id, activatedAt: partnerAccess.activatedAt, }); - expect(crispApi.updateCrispProfileAccesses).toHaveBeenCalledWith( - mockGetUserDto.user, - [partnerAccess], - [], - ); + expect(crispApi.updateCrispProfileAccesses).toHaveBeenCalledWith(mockUserEntity, [ + partnerAccess, + ]); }); it('should assign partner access even if crisp profile api fails', async () => { // Mocks that the accesscode already exists jest.spyOn(repo, 'findOne').mockResolvedValueOnce(mockPartnerAccessEntity); - jest.spyOn(crispApi, 'updateCrispProfileAccesses').mockImplementationOnce(async () => { + jest.spyOn(crispApi, 'updateCrispProfileData').mockImplementationOnce(async () => { throw new Error('Test throw'); }); - const partnerAccess = await service.assignPartnerAccess(mockGetUserDto, '123456'); + const partnerAccess = await service.assignPartnerAccess(mockUserEntity, '123456'); expect(partnerAccess).toEqual({ ...mockPartnerAccessEntity, - userId: mockGetUserDto.user.id, + userId: mockUserEntity.id, activatedAt: partnerAccess.activatedAt, }); }); @@ -177,7 +175,7 @@ describe('PartnerAccessService', () => { userId: 'anotherUserId', }); - await expect(service.assignPartnerAccess(mockGetUserDto, '123456')).rejects.toThrow( + await expect(service.assignPartnerAccess(mockUserEntity, '123456')).rejects.toThrow( PartnerAccessCodeStatusEnum.ALREADY_IN_USE, ); }); @@ -189,7 +187,7 @@ describe('PartnerAccessService', () => { userId: mockGetUserDto.user.id, }); - await expect(service.assignPartnerAccess(mockGetUserDto, '123456')).rejects.toThrow( + await expect(service.assignPartnerAccess(mockUserEntity, '123456')).rejects.toThrow( PartnerAccessCodeStatusEnum.ALREADY_APPLIED, ); }); diff --git a/src/partner-access/partner-access.service.ts b/src/partner-access/partner-access.service.ts index fa524ac3..62fe91a7 100644 --- a/src/partner-access/partner-access.service.ts +++ b/src/partner-access/partner-access.service.ts @@ -184,9 +184,14 @@ export class PartnerAccessService { userId: user.id, activatedAt: new Date(), }); + assignedPartnerAccess.partner = partnerAccess.partner; try { - await updateCrispProfileAccesses(user, [...user.partnerAccess, assignedPartnerAccess]); + const partnerAccesses = await this.partnerAccessRepository.findBy({ + userId: user.id, + active: true, + }); + updateCrispProfileAccesses(user, partnerAccesses); } catch (error) { this.logger.error( `Error: Unable to update crisp profile for ${user.email}. Error: ${error.message} `, diff --git a/src/user/user.service.spec.ts b/src/user/user.service.spec.ts index 3b941d40..a70dd94b 100644 --- a/src/user/user.service.spec.ts +++ b/src/user/user.service.spec.ts @@ -125,6 +125,7 @@ describe('UserService', () => { const user = await service.createUser({ ...createUserDto, + partnerId: mockPartnerEntity.id, partnerAccessCode: mockPartnerAccessEntity.accessCode, }); @@ -208,10 +209,12 @@ describe('UserService', () => { }) as never, ); - const user = await service.getUserByFirebaseId(mockIFirebaseUser); - expect(user.user.email).toBe('user@email.com'); - expect(user.partnerAdmin).toBeNull(); - expect(user.partnerAccesses).toEqual([]); + const userResponse = await service.getUserByFirebaseId(mockIFirebaseUser); + expect(userResponse.userEntity.email).toBe('user@email.com'); + expect(userResponse.userDto.user.email).toBe('user@email.com'); + expect(userResponse.userDto.user.email).toBe('user@email.com'); + expect(userResponse.userDto.partnerAdmin).toBeNull(); + expect(userResponse.userDto.partnerAccesses).toEqual([]); }); }); diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 8dcbc4d8..18ddaf03 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -41,8 +41,8 @@ export class UserService { ? SIGNUP_TYPE.PARTNER_USER_WITHOUT_CODE : SIGNUP_TYPE.PUBLIC_USER; - let partnerAccess: PartnerAccessEntity | null; - const partner = await this.partnerRepository.findOneBy({ id: partnerId }); + let partnerAccess: PartnerAccessEntity | null = null; + const partner = partnerId ? await this.partnerRepository.findOneBy({ id: partnerId }) : null; try { if (signUpType === SIGNUP_TYPE.PARTNER_USER_WITHOUT_CODE) { @@ -93,9 +93,7 @@ export class UserService { } } - public async getUserByFirebaseId({ - uid, - }: IFirebaseUser): Promise<{ + public async getUserByFirebaseId({ uid }: IFirebaseUser): Promise<{ userEntity: UserEntity | undefined; userDto: GetUserDto | undefined; }> { diff --git a/src/utils/profileData.ts b/src/utils/profileData.ts index 7ddb60f2..0966b9fb 100644 --- a/src/utils/profileData.ts +++ b/src/utils/profileData.ts @@ -2,8 +2,8 @@ import { addCrispProfile, updateCrispProfileData } from 'src/api/crisp/crisp-api import { CourseUserEntity } from 'src/entities/course-user.entity'; import { PartnerAccessEntity } from 'src/entities/partner-access.entity'; import { PartnerEntity } from 'src/entities/partner.entity'; -import { SessionUserEntity } from 'src/entities/session-user.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { PROGRESS_STATUS } from './constants'; export const getAcronym = (text: string) => { return `${text @@ -12,22 +12,11 @@ export const getAcronym = (text: string) => { .toLowerCase()}`; }; -export const formatCourseUserValue = (courseUser: CourseUserEntity) => { - return `${getAcronym(courseUser.course.name)}:${courseUser.completed ? 'C' : 'S'}`; -}; - -export const formatSessionUserValue = (sessionUser: SessionUserEntity) => { - return `${getAcronym(sessionUser.session.name)}:${sessionUser.completed ? 'C' : 'S'}`; -}; - export const createServicesProfiles = async ( user: UserEntity, partner: PartnerEntity, partnerAccess: PartnerAccessEntity, - courses?: CourseUserEntity[], ) => { - if (partnerAccess) partnerAccess.partner = partner; - await addCrispProfile({ email: user.email, person: { nickname: user.name }, @@ -37,8 +26,7 @@ export const createServicesProfiles = async ( await updateCrispProfileData( { ...serializeUserData(user), - ...serializePartnerAccessData([partnerAccess]), - ...(courses?.length && { ...serializeCourseData(courses) }), + ...(partnerAccess && serializePartnerAccessData([{ ...partnerAccess, partner }])), }, user.email, ); @@ -55,7 +43,7 @@ export const serializeUserData = (user: UserEntity) => { export const serializePartnerAccessData = (partnerAccesses: PartnerAccessEntity[]) => { const partnerAccessData = { - partners: partnerAccesses.map((pa) => pa.partner.name).join('; ') || '', + partners: partnerAccesses.map((pa) => pa.partner?.name || '').join('; ') || '', feature_live_chat: !!partnerAccesses.find((pa) => !!pa.featureLiveChat), feature_therapy: !!partnerAccesses.find((pa) => !!pa.featureTherapy), therapy_sessions_remaining: partnerAccesses @@ -65,20 +53,23 @@ export const serializePartnerAccessData = (partnerAccesses: PartnerAccessEntity[ .map((pa) => pa.therapySessionsRedeemed) .reduce((a, b) => a + b, 0), }; + return partnerAccessData; }; -export const serializeCourseData = (courseUsers?: CourseUserEntity[]) => { - const sessionKeyValues = {}; - - courseUsers.forEach((courseUser) => { - sessionKeyValues[`course_${getAcronym(courseUser.course.name)}_sessions`] = - courseUser.sessionUser.map((sessionUser) => formatSessionUserValue(sessionUser)).join('; '); - }); - +export const serializeCourseData = (courseUser: CourseUserEntity) => { + // Returns e.g. { course_IBA: "Started", course_IBA_sessions: "IBDP:Started; DOC:Completed"} const courseData = { - courses: courseUsers.map((courseUser) => formatCourseUserValue(courseUser)).join(';'), - ...sessionKeyValues, + [`course_${getAcronym(courseUser.course.name)}`]: courseUser.completed + ? PROGRESS_STATUS.COMPLETED + : PROGRESS_STATUS.STARTED, + + [`course_${getAcronym(courseUser.course.name)}_sessions`]: courseUser.sessionUser + .map( + (sessionUser) => + `${getAcronym(sessionUser.session.name)}:${sessionUser.completed ? 'C' : 'S'}`, + ) + .join('; '), }; return courseData; diff --git a/src/webhooks/webhooks.service.spec.ts b/src/webhooks/webhooks.service.spec.ts index b2aa0e16..0f8adf3c 100644 --- a/src/webhooks/webhooks.service.spec.ts +++ b/src/webhooks/webhooks.service.spec.ts @@ -89,6 +89,9 @@ jest.mock('../api/crisp/crisp-api', () => { updateCrispProfileData: () => { return; }, + updateCrispProfileTherapy: () => { + return; + }, getCrispPeopleData: () => { return { error: false, diff --git a/src/webhooks/webhooks.service.ts b/src/webhooks/webhooks.service.ts index f0d1b41e..aef01255 100644 --- a/src/webhooks/webhooks.service.ts +++ b/src/webhooks/webhooks.service.ts @@ -1,6 +1,7 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { createHmac } from 'crypto'; +import { updateCrispProfileTherapy } from 'src/api/crisp/crisp-api'; import { SlackMessageClient } from 'src/api/slack/slack-api'; import { CourseEntity } from 'src/entities/course.entity'; import { EventLogEntity } from 'src/entities/event-log.entity'; @@ -15,7 +16,6 @@ import { serializeZapierSimplyBookDtoToTherapySessionEntity } from 'src/utils/se import { WebhookCreateEventLogDto } from 'src/webhooks/dto/webhook-create-event-log.dto'; import StoryblokClient from 'storyblok-js-client'; import { ILike, Repository } from 'typeorm'; -import { getCrispPeopleData, updateCrispProfileData } from '../api/crisp/crisp-api'; import { CoursePartnerService } from '../course-partner/course-partner.service'; import { SIMPLYBOOK_ACTION_ENUM, @@ -42,28 +42,6 @@ export class WebhooksService { private slackMessageClient: SlackMessageClient, ) {} - async updateCrispProfileTherapyData(action, email) { - let partnerAccessUpdateCrisp = {}; - const crispResponse = await getCrispPeopleData(email); - const crispData = crispResponse.data.data.data; - - if (action === SIMPLYBOOK_ACTION_ENUM.NEW_BOOKING) { - partnerAccessUpdateCrisp = { - therapy_sessions_remaining: crispData['therapy_sessions_remaining'] - 1, - therapy_sessions_redeemed: crispData['therapy_sessions_redeemed'] + 1, - }; - } - - if (action === SIMPLYBOOK_ACTION_ENUM.CANCELLED_BOOKING) { - partnerAccessUpdateCrisp = { - therapy_sessions_remaining: crispData['therapy_sessions_remaining'] + 1, - therapy_sessions_redeemed: crispData['therapy_sessions_redeemed'] - 1, - }; - } - - updateCrispProfileData(partnerAccessUpdateCrisp, email); - } - async updatePartnerAccessTherapy( simplyBookDto: ZapierSimplybookBodyDto, ): Promise { @@ -106,8 +84,6 @@ export class WebhooksService { // Updating an existing therapy session existingTherapySession.action = action; - this.updateCrispProfileTherapyData(action, user.email); - // If the booking is cancelled, increment the therapy sessions remaining on related partner access if (action === SIMPLYBOOK_ACTION_ENUM.CANCELLED_BOOKING) { try { @@ -140,6 +116,14 @@ export class WebhooksService { try { const therapySession = await this.therapySessionRepository.save(existingTherapySession); + + const partnerAccesses = await this.partnerAccessRepository.findBy({ + userId: user.id, + active: true, + featureTherapy: true, + }); + updateCrispProfileTherapy(partnerAccesses, user.email); + this.logger.log( `Update therapy session webhook function COMPLETED for ${action} - ${user.email} - ${booking_code} - userId ${user_id}`, ); @@ -236,11 +220,11 @@ export class WebhooksService { throw new HttpException(error, HttpStatus.FORBIDDEN); } - this.updateCrispProfileTherapyData(SIMPLYBOOK_ACTION_ENUM.NEW_BOOKING, user.email); - partnerAccess.therapySessionsRemaining -= 1; partnerAccess.therapySessionsRedeemed += 1; + updateCrispProfileTherapy([...partnerAccesses, partnerAccess], user.email); + try { const serializedTherapySession = serializeZapierSimplyBookDtoToTherapySessionEntity( simplyBookDto,