Skip to content

Commit

Permalink
feat: add serviceEmailsPermission field (#390)
Browse files Browse the repository at this point in the history
  • Loading branch information
annarhughes committed Jan 25, 2024
1 parent b238f4c commit 2dfd39e
Show file tree
Hide file tree
Showing 14 changed files with 140 additions and 11 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ To revert a migration
yarn migration:revert
```

Note that when running the app in Docker, you may need to run migration commands from the docker terminal/Exec

**New environment variables must be added to Heroku before release.**

### Run unit tests
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js",
"seed": "bash ./staging_backup.sh -c",
"migration:generate": "yarn build && yarn typeorm migration:generate -- -n bloom-backend",
"migration:generate": "yarn build && yarn typeorm -- migration:generate -n bloom-backend",
"migration:run": "yarn build && yarn typeorm -- migration:run",
"migration:revert": "yarn typeorm -- migration:revert",
"migration:show": "yarn build && yarn typeorm -- migration:show",
"prepare": "husky install"
},
"dependencies": {
Expand Down
1 change: 1 addition & 0 deletions src/api/crisp/utils/createCrispProfileData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const userDto = {
name: 'name',
firebaseUid: 'iiiiod',
contactPermission: false,
serviceEmailsPermission: true,
id: 'string',
createdAt: new Date(),
updatedAt: new Date(),
Expand Down
7 changes: 5 additions & 2 deletions src/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ export class UserEntity extends BaseBloomEntity {
@Column({ nullable: true })
signUpLanguage: string;

@Column()
contactPermission!: boolean;
@Column({ default: false })
contactPermission: boolean; // marketing consent

@Column({ default: true })
serviceEmailsPermission: boolean; // service emails consent

@Column({ type: Boolean, default: false })
isSuperAdmin: boolean;
Expand Down
19 changes: 19 additions & 0 deletions src/migrations/1706174260018-bloom-backend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class bloomBackend1706174260018 implements MigrationInterface {
name = 'bloomBackend1706174260018';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user" ADD "serviceEmailsPermission" boolean NOT NULL DEFAULT true`,
);
await queryRunner.query(
`ALTER TABLE "user" ALTER COLUMN "contactPermission" SET DEFAULT false`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "contactPermission" DROP DEFAULT`);
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "serviceEmailsPermission"`);
}
}
1 change: 1 addition & 0 deletions src/partner-admin/partner-admin-auth.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const userEntity: UserEntity = {
email: 'usermail',
name: 'name',
contactPermission: false,
serviceEmailsPermission: true,
isSuperAdmin: false,
crispTokenId: '123',
partnerAccess: [],
Expand Down
3 changes: 2 additions & 1 deletion src/partner-admin/partner-admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class PartnerAdminService {
email,
firebaseUid: firebaseUser.user.uid,
contactPermission: true,
serviceEmailsPermission: true,
});

return await this.partnerAdminRepository.save({
Expand Down Expand Up @@ -88,7 +89,7 @@ export class PartnerAdminService {
.update(PartnerAdminEntity)
.set({ active: updatePartnerAdminDto.active })
.where('partnerAdminId = :partnerAdminId', { partnerAdminId })
.returning('*')
.returning('*');
} catch (error) {
throw error;
}
Expand Down
5 changes: 5 additions & 0 deletions src/user/dtos/create-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export class CreateUserDto {
@ApiProperty({ type: Boolean })
contactPermission: boolean;

@IsOptional()
@IsBoolean()
@ApiProperty({ type: Boolean })
serviceEmailsPermission: boolean;

@IsOptional()
@IsString()
@ApiProperty({ type: String })
Expand Down
5 changes: 5 additions & 0 deletions src/user/dtos/update-user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ export class UpdateUserDto {
@IsOptional()
@ApiProperty({ type: Boolean })
contactPermission: boolean;

@IsBoolean()
@IsOptional()
@ApiProperty({ type: Boolean })
serviceEmailsPermission: boolean;
}
6 changes: 6 additions & 0 deletions src/user/user.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,23 @@ const createUserDto: CreateUserDto = {
password: 'password',
name: 'name',
contactPermission: false,
serviceEmailsPermission: true,
signUpLanguage: 'en',
};

const createUserRepositoryDto = {
email: '[email protected]',
name: 'name',
contactPermission: false,
serviceEmailsPermission: true,
signUpLanguage: 'en',
firebaseUid: mockUserRecord.uid,
};

const updateUserDto: UpdateUserDto = {
name: 'new name',
contactPermission: true,
serviceEmailsPermission: false,
};

const mockPartnerWithAutomaticAccessCodeFeature = {
Expand Down Expand Up @@ -240,6 +244,7 @@ describe('UserService', () => {
expect(user.name).toBe('new name');
expect(user.email).toBe('[email protected]');
expect(user.contactPermission).toBe(true);
expect(user.serviceEmailsPermission).toBe(false);

expect(repoSpySave).toBeCalledWith({ ...mockUserEntity, ...updateUserDto });
expect(repoSpySave).toBeCalled();
Expand Down Expand Up @@ -280,6 +285,7 @@ describe('UserService', () => {
partnerAccess,
signUpLanguage,
contactPermission,
serviceEmailsPermission,
courseUser,
eventLog,
...userBase
Expand Down
25 changes: 22 additions & 3 deletions src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export class UserService {
}

public async createPublicUser(
{ name, email, contactPermission, signUpLanguage }: CreateUserDto,
{ name, email, contactPermission, serviceEmailsPermission, signUpLanguage }: CreateUserDto,
firebaseUid: string,
) {
try {
Expand All @@ -169,6 +169,7 @@ export class UserService {
email,
firebaseUid,
contactPermission,
serviceEmailsPermission,
signUpLanguage,
});
const createUserResponse = await this.userRepository.save(createUserObject);
Expand All @@ -180,7 +181,14 @@ export class UserService {
}

public async createPartnerUserWithoutCode(
{ name, email, contactPermission, signUpLanguage, partnerId }: CreateUserDto,
{
name,
email,
contactPermission,
serviceEmailsPermission,
signUpLanguage,
partnerId,
}: CreateUserDto,
firebaseUid: string,
) {
try {
Expand All @@ -204,6 +212,7 @@ export class UserService {
email,
firebaseUid,
contactPermission,
serviceEmailsPermission,
signUpLanguage,
});

Expand All @@ -223,7 +232,14 @@ export class UserService {
}

public async createPartnerUserWithCode(
{ name, email, contactPermission, signUpLanguage, partnerAccessCode }: CreateUserDto,
{
name,
email,
contactPermission,
serviceEmailsPermission,
signUpLanguage,
partnerAccessCode,
}: CreateUserDto,
firebaseUid: string,
) {
try {
Expand All @@ -236,6 +252,7 @@ export class UserService {
email,
firebaseUid,
contactPermission,
serviceEmailsPermission,
signUpLanguage,
});

Expand Down Expand Up @@ -349,6 +366,8 @@ export class UserService {

user.name = updateUserDto?.name ?? user.name;
user.contactPermission = updateUserDto?.contactPermission ?? user.contactPermission;
user.serviceEmailsPermission =
updateUserDto?.serviceEmailsPermission ?? user.serviceEmailsPermission;

await this.userRepository.save(user);

Expand Down
58 changes: 54 additions & 4 deletions src/webhooks/webhooks.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
mockSessionStoryblokResult,
mockSimplybookBodyBase,
mockTherapySessionEntity,
mockUserEntity,
} from 'test/utils/mockData';
import {
mockCoursePartnerRepositoryMethods,
Expand Down Expand Up @@ -680,7 +681,7 @@ describe('WebhooksService', () => {
});
});
describe('sendFirstTherapySessionFeedbackEmail', () => {
it('should send email to those with bookings yesterday', async () => {
it('should send emails to users with bookings yesterday', async () => {
jest.spyOn(mockedEmailCampaignRepository, 'find').mockImplementationOnce(async () => {
return [];
});
Expand All @@ -693,7 +694,7 @@ describe('WebhooksService', () => {
);
});

it('should send email to only those who have not recieved an email already', async () => {
it('should send emails to only users who have not received an email already', async () => {
// Mocking that email campaign entry already exists
jest.spyOn(mockedEmailCampaignRepository, 'find').mockImplementationOnce(async () => {
return [{} as EmailCampaignEntity];
Expand All @@ -713,7 +714,8 @@ describe('WebhooksService', () => {
await expect(mailChimpSpy).toBeCalledTimes(0);
await expect(saveSpy).toBeCalledTimes(0);
});
it('should only send bookings to those who have signed up in english', async () => {

it('should only send emails to users who have signed up in english', async () => {
jest.spyOn(mockedEmailCampaignRepository, 'find').mockImplementationOnce(async () => {
return [];
});
Expand All @@ -730,9 +732,31 @@ describe('WebhooksService', () => {
)}`,
);
});

it('should not send emails to users who have disabled service emails', async () => {
jest.spyOn(mockedEmailCampaignRepository, 'find').mockImplementationOnce(async () => {
return [];
});
jest
.spyOn(mockedTherapySessionRepository, 'findOneOrFail')
.mockImplementationOnce(async () => {
return {
...mockTherapySessionEntity,
user: { serviceEmailsPermission: false } as UserEntity,
};
});
const sentEmails = await service.sendFirstTherapySessionFeedbackEmail();
expect(sentEmails).toBe(
`First therapy session feedback emails sent to 0 client(s) for date: ${format(
sub(new Date(), { days: 1 }),
'dd/MM/yyyy',
)}`,
);
});
});

describe('sendImpactMeasurementEmail', () => {
it('should send email to those with bookings yesterday', async () => {
it('should send email to users with bookings yesterday', async () => {
const startDate = sub(startOfDay(new Date()), { days: 180 });
const endDate = sub(startOfDay(new Date()), { days: 173 });
const mailChimpSpy = jest.spyOn(mockedMailchimpClient, 'sendImpactMeasurementEmail');
Expand Down Expand Up @@ -815,6 +839,7 @@ describe('WebhooksService', () => {
expect(emailCampaignRepositorySpy).toBeCalledTimes(1);
expect(emailCampaignRepositoryFindSpy).toBeCalledTimes(2);
});

it('if a user has already been sent an email, no second email is sent', async () => {
const startDate = sub(startOfDay(new Date()), { days: 180 });
const endDate = sub(startOfDay(new Date()), { days: 173 });
Expand All @@ -840,6 +865,30 @@ describe('WebhooksService', () => {
expect(emailCampaignRepositoryFindSpy).toBeCalledTimes(2);
});

it('if a user disabled service emails, no email is sent', async () => {
const startDate = sub(startOfDay(new Date()), { days: 180 });
const endDate = sub(startOfDay(new Date()), { days: 173 });
const emailCampaignRepositorySpy = jest.spyOn(mockedEmailCampaignRepository, 'save');

jest.spyOn(mockedUserRepository, 'find').mockImplementationOnce(async () => {
return [
{
...mockUserEntity,
serviceEmailsPermission: false,
},
];
});
const sentEmails = await service.sendImpactMeasurementEmail();

expect(sentEmails).toBe(
`Impact feedback email sent to ${0} users who created their account between ${format(
startDate,
'dd/MM/yyyy',
)} - ${format(endDate, 'dd/MM/yyyy')}`,
);
expect(emailCampaignRepositorySpy).toBeCalledTimes(0);
});

it('if error occurs fetching users, error is thrown', async () => {
jest.spyOn(mockedUserRepository, 'find').mockImplementationOnce(async () => {
throw new Error('Failed to save');
Expand All @@ -849,6 +898,7 @@ describe('WebhooksService', () => {
);
});
});

describe('createEventLog', () => {
it('should create an eventLog if DTO is correct', async () => {
const eventDto: WebhookCreateEventLogDto = {
Expand Down
15 changes: 15 additions & 0 deletions src/webhooks/webhooks.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ export class WebhooksService {
continue;
}

if (therapySession.user && therapySession.user.serviceEmailsPermission === false) {
const emailLog = `Therapy session feedback email not sent as user has disabled service emails [email: ${
booking.clientEmail
}, session date: ${format(sub(new Date(), { days: 1 }), 'dd/MM/yyyy')}]`;
this.logger.log(emailLog);
this.slackMessageClient.sendMessageToTherapySlackChannel(emailLog);
continue;
}

await this.mailchimpClient.sendTherapyFeedbackEmail(booking.clientEmail);
const emailLog = `First therapy session feedback email sent [email: ${
booking.clientEmail
Expand Down Expand Up @@ -164,6 +173,12 @@ export class WebhooksService {
);
continue;
}
if (user.serviceEmailsPermission === false) {
this.logger.log(
`sendImpactMeasurementEmail: Skipped sending user Impact Measurement Email - user has disabled service emails [email: ${user.email}]`,
);
continue;
}
} catch (err) {
this.logger.error(
`sendImpactMeasurementEmail: Failed to find user in emailCampaignRepository [email: ${user.email}]`,
Expand Down
1 change: 1 addition & 0 deletions test/utils/mockData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export const mockUserEntity: UserEntity = {
crispTokenId: '123',
firebaseUid: '123',
contactPermission: true,
serviceEmailsPermission: true,
email: '[email protected]',
name: 'name',
signUpLanguage: 'en',
Expand Down

0 comments on commit 2dfd39e

Please sign in to comment.