diff --git a/package.json b/package.json index 3621d3d2..4d479245 100644 --- a/package.json +++ b/package.json @@ -41,11 +41,11 @@ "axios": "^0.24.0", "class-transformer": "^0.4.0", "class-validator": "^0.14.0", + "date-fns": "^2.30.0", "dotenv": "^10.0.0", "firebase": "^9.4.1", "firebase-admin": "^10.0.0", "lodash": "^4.17.21", - "moment": "^2.29.4", "passport": "^0.5.0", "passport-firebase-jwt": "^1.2.1", "pg": "^8.7.1", diff --git a/src/partner-access/partner-access.service.spec.ts b/src/partner-access/partner-access.service.spec.ts index 51070f4e..9819160d 100644 --- a/src/partner-access/partner-access.service.spec.ts +++ b/src/partner-access/partner-access.service.spec.ts @@ -1,15 +1,19 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { sub } from 'date-fns'; import * as crispApi from 'src/api/crisp/crisp-api'; import { PartnerRepository } from 'src/partner/partner.repository'; import { GetUserDto } from 'src/user/dtos/get-user.dto'; import { mockPartnerAccessEntity, + mockPartnerAccessEntityBase, mockPartnerEntity, mockUserEntity, - partnerAccessArray, } from 'test/utils/mockData'; -import { mockPartnerRepositoryMethods } from 'test/utils/mockedServices'; +import { + mockPartnerAccessRepositoryMethods, + mockPartnerRepositoryMethods, +} from 'test/utils/mockedServices'; import { Repository } from 'typeorm'; import { createQueryBuilderMock } from '../../test/utils/mockUtils'; import { PartnerAccessEntity } from '../entities/partner-access.entity'; @@ -28,21 +32,6 @@ const createPartnerAccessDto: CreatePartnerAccessDto = { therapySessionsRemaining: 5, }; -const partnerAccessEntityBase = { - id: 'randomId', - userId: null, - partnerId: '', - partnerAdminId: null, - user: null, - partnerAdmin: null, - partner: null, - active: false, - activatedAt: null, - accessCode: null, - createdAt: new Date(), - therapySession: [], - updatedAt: null, -}; const mockGetUserDto = { user: mockUserEntity, partnerAccesses: [], @@ -62,26 +51,20 @@ describe('PartnerAccessService', () => { let service: PartnerAccessService; let repo: PartnerAccessRepository; let mockPartnerRepository: DeepMocked; + let mockPartnerAccessRepository: DeepMocked; beforeEach(async () => { mockPartnerRepository = createMock(mockPartnerRepositoryMethods); + mockPartnerAccessRepository = createMock( + mockPartnerAccessRepositoryMethods, + ); const module: TestingModule = await Test.createTestingModule({ providers: [ PartnerAccessService, { provide: PartnerAccessRepository, - useFactory: jest.fn(() => ({ - createQueryBuilder: createQueryBuilderMock(), - create: (dto: CreatePartnerAccessDto): PartnerAccessEntity | Error => { - return { - ...partnerAccessEntityBase, - ...dto, - }; - }, - find: jest.fn(() => partnerAccessArray), - save: jest.fn((arg) => arg), - })), + useValue: mockPartnerAccessRepository, }, { provide: PartnerRepository, @@ -107,7 +90,7 @@ describe('PartnerAccessService', () => { partnerId, partnerAdminId, ); - const { accessCode, ...partnerEntityWithoutCode } = partnerAccessEntityBase; + const { accessCode, ...partnerEntityWithoutCode } = mockPartnerAccessEntityBase; expect(generatedCode).toStrictEqual({ ...partnerEntityWithoutCode, ...createPartnerAccessDto, @@ -230,4 +213,38 @@ describe('PartnerAccessService', () => { expect(partnerAccesses.length).toBe(1); }); }); + + describe('getValidPartnerAccessCode', () => { + it('when a valid partner access is supplied, it should return partner access', async () => { + jest.spyOn(repo, 'createQueryBuilder').mockImplementationOnce( + createQueryBuilderMock({ + leftJoinAndSelect: jest.fn().mockReturnThis(), + getOne: jest.fn().mockResolvedValue(mockPartnerAccessEntity), + }) as never, + ); + const partnerAccess = await service.getValidPartnerAccessCode('123456'); + expect(partnerAccess).toHaveProperty('accessCode', '123456'); + }); + + it('when a valid partner access is supplied, but it was created over a year ago, it should throw error', async () => { + jest.spyOn(repo, 'createQueryBuilder').mockImplementationOnce( + createQueryBuilderMock({ + leftJoinAndSelect: jest.fn().mockReturnThis(), + + getOne: jest.fn().mockResolvedValue({ + ...mockPartnerAccessEntity, + createdAt: sub(new Date(), { years: 1, days: 1 }), + }), + }) as never, + ); + await expect(service.getValidPartnerAccessCode('123456')).rejects.toThrowError( + 'CODE_EXPIRED', + ); + }); + it('when an partner access with too many letters is supplied, it should throw error', async () => { + await expect(service.getValidPartnerAccessCode('1234567')).rejects.toThrowError( + 'INVALID_CODE', + ); + }); + }); }); diff --git a/src/partner-access/partner-access.service.ts b/src/partner-access/partner-access.service.ts index 6e4081ea..ccd08c38 100644 --- a/src/partner-access/partner-access.service.ts +++ b/src/partner-access/partner-access.service.ts @@ -1,7 +1,7 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { isBefore, sub } from 'date-fns'; import _ from 'lodash'; -import moment from 'moment'; import { PartnerEntity } from 'src/entities/partner.entity'; import { Logger } from 'src/logger/logger'; import { PartnerRepository } from 'src/partner/partner.repository'; @@ -81,7 +81,8 @@ export class PartnerAccessService { throw new HttpException(PartnerAccessCodeStatusEnum.ALREADY_IN_USE, HttpStatus.CONFLICT); } - if (moment(partnerAccess.createdAt).add(1, 'year').isSameOrBefore(Date.now())) { + // ensure the partner access code has been created no more than a year ago + if (isBefore(new Date(partnerAccess.createdAt), sub(new Date(), { years: 1 }))) { throw new HttpException(PartnerAccessCodeStatusEnum.CODE_EXPIRED, HttpStatus.BAD_REQUEST); } diff --git a/src/utils/serialize.spec.ts b/src/utils/serialize.spec.ts new file mode 100644 index 00000000..2ccb9332 --- /dev/null +++ b/src/utils/serialize.spec.ts @@ -0,0 +1,25 @@ +import { mockSimplybookBodyBase } from 'test/utils/mockData'; +import { formatTherapySessionObject } from './serialize'; + +describe('Serialize', () => { + describe('formatTherapySessionObject', () => { + it('should format object correctly when valid object is supplied', () => { + const randomString = formatTherapySessionObject(mockSimplybookBodyBase, 'partnerAccessId'); + expect(randomString).toEqual({ + action: 'UPDATED_BOOKING', + bookingCode: 'abc', + cancelledAt: null, + clientEmail: 'testuser@test.com', + clientTimezone: 'Europe/London', + completedAt: null, + endDateTime: new Date('2022-09-12T08:30:00+0000'), + partnerAccessId: 'partnerAccessId', + rescheduledFrom: null, + serviceName: 'bloom therapy', + serviceProviderEmail: 'therapist@test.com', + serviceProviderName: 'therapist@test.com', + startDateTime: new Date('2022-09-12T07:30:00+0000'), + }); + }); + }); +}); diff --git a/src/utils/serialize.ts b/src/utils/serialize.ts index b32a25c1..55a7945e 100644 --- a/src/utils/serialize.ts +++ b/src/utils/serialize.ts @@ -1,4 +1,3 @@ -import moment from 'moment'; import { PartnerEntity } from 'src/entities/partner.entity'; import { IPartnerFeature } from 'src/partner-feature/partner-feature.interface'; import { IPartner } from 'src/partner/partner.interface'; @@ -136,8 +135,8 @@ export const formatTherapySessionObject = ( serviceName: therapySession.service_name, serviceProviderName: therapySession.service_provider_email, serviceProviderEmail: therapySession.service_provider_email, - startDateTime: moment(therapySession.start_date_time).toDate(), - endDateTime: moment(therapySession.end_date_time).toDate(), + startDateTime: new Date(therapySession.start_date_time), + endDateTime: new Date(therapySession.end_date_time), cancelledAt: null, rescheduledFrom: null, completedAt: null, diff --git a/src/webhooks/webhooks.service.spec.ts b/src/webhooks/webhooks.service.spec.ts index dc352b0d..c0b48476 100644 --- a/src/webhooks/webhooks.service.spec.ts +++ b/src/webhooks/webhooks.service.spec.ts @@ -391,14 +391,16 @@ describe('WebhooksService', () => { describe('updatePartnerAccessTherapy', () => { it('should update the booking time when action is update and time is different TODO ', async () => { const newStartTime = '2022-09-12T09:30:00+0000'; + const newEndTime = '2022-09-12T10:30:00+0000'; const therapyRepoFindOneSpy = jest.spyOn(mockedTherapySessionRepository, 'findOne'); const booking = await service.updatePartnerAccessTherapy({ ...mockSimplybookBodyBase, start_date_time: newStartTime, - end_date_time: '2022-09-12T010:30:00+0000', + end_date_time: newEndTime, action: SIMPLYBOOK_ACTION_ENUM.UPDATED_BOOKING, }); expect(booking).toHaveProperty('startDateTime', new Date(newStartTime)); + expect(booking).toHaveProperty('endDateTime', new Date(newEndTime)); expect(therapyRepoFindOneSpy).toBeCalled(); }); diff --git a/src/webhooks/webhooks.service.ts b/src/webhooks/webhooks.service.ts index 7510e9e8..5dcdf579 100644 --- a/src/webhooks/webhooks.service.ts +++ b/src/webhooks/webhooks.service.ts @@ -1,6 +1,5 @@ import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import moment from 'moment'; import { MailchimpClient } from 'src/api/mailchimp/mailchip-api'; import { getBookingsForDate } from 'src/api/simplybook/simplybook-api'; import { SlackMessageClient } from 'src/api/slack/slack-api'; @@ -134,8 +133,8 @@ export class WebhooksService { ...(action === SIMPLYBOOK_ACTION_ENUM.UPDATED_BOOKING ? { rescheduledFrom: therapySession.startDateTime, - startDateTime: moment(simplyBookDto.start_date_time).toDate(), - endDateTime: moment(simplyBookDto.end_date_time).toDate(), + startDateTime: new Date(simplyBookDto.start_date_time), + endDateTime: new Date(simplyBookDto.end_date_time), } : {}), ...(action === SIMPLYBOOK_ACTION_ENUM.COMPLETED_BOOKING ? { completedAt: new Date() } : {}), diff --git a/test/utils/mockData.ts b/test/utils/mockData.ts index 225085c3..40a760e7 100644 --- a/test/utils/mockData.ts +++ b/test/utils/mockData.ts @@ -179,6 +179,21 @@ export const mockPartnerEntity = { partnerFeature: [], } as PartnerEntity; +export const mockPartnerAccessEntityBase = { + id: 'randomId', + userId: null, + partnerId: '', + partnerAdminId: null, + user: null, + partnerAdmin: null, + partner: null, + active: false, + activatedAt: null, + accessCode: null, + createdAt: new Date(), + therapySession: [], + updatedAt: null, +}; export const mockPartnerAccessEntity = { id: 'pa1', therapySessionsRemaining: 5, diff --git a/test/utils/mockedServices.ts b/test/utils/mockedServices.ts index e838a410..5ff237f6 100644 --- a/test/utils/mockedServices.ts +++ b/test/utils/mockedServices.ts @@ -29,12 +29,14 @@ import { mockEmailCampaignEntity, mockFeatureEntity, mockPartnerAccessEntity, + mockPartnerAccessEntityBase, mockPartnerEntity, mockPartnerFeatureEntity, mockSession, mockTherapySessionEntity, mockUserEntity, mockUserRecord, + partnerAccessArray, } from './mockData'; import { createQueryBuilderMock } from './mockUtils'; @@ -111,15 +113,18 @@ export const mockPartnerAccessRepositoryMethods: PartialFuncReturn { return { - ...mockPartnerAccessEntity, + ...mockPartnerAccessEntityBase, ...dto, - } as PartnerAccessEntity; + }; }, findOne: async (arg) => { return { ...mockPartnerAccessEntity, ...(arg ? { ...arg } : {}) } as PartnerAccessEntity; }, find: async (arg) => { - return [{ ...mockPartnerAccessEntity, ...(arg ? { ...arg } : {}) }] as PartnerAccessEntity[]; + return [ + ...partnerAccessArray, + { ...mockPartnerAccessEntity, ...(arg ? { ...arg } : {}) }, + ] as PartnerAccessEntity[]; }, save: async (arg) => arg as PartnerAccessEntity, }; diff --git a/yarn.lock b/yarn.lock index ca2807fe..14d79dca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -326,6 +326,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.14.5" +"@babel/runtime@^7.21.0": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.15.tgz#38f46494ccf6cf020bd4eed7124b425e83e523b8" + integrity sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.15.4", "@babel/template@^7.3.3": version "7.15.4" resolved "https://registry.npmjs.org/@babel/template/-/template-7.15.4.tgz" @@ -2672,6 +2679,13 @@ date-and-time@^2.0.0: resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-2.0.1.tgz#bc8b72704980e8a0979bb186118d30d02059ef04" integrity sha512-O7Xe5dLaqvY/aF/MFWArsAM1J4j7w1CSZlPCX9uHgmb+6SbkPd8Q4YOvfvH/cZGvFlJFfHOZKxQtmMUOoZhc/w== +date-fns@^2.30.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + debug@2.6.9: version "2.6.9" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" @@ -4949,11 +4963,6 @@ mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment@^2.29.4: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== - ms@2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" @@ -5639,6 +5648,11 @@ reflect-metadata@^0.1.13: resolved "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz" integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regexpp@^3.1.0: version "3.2.0" resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz"