diff --git a/migrations/1660540253547-createNotificationType.ts b/migrations/1660540253547-createNotificationType.ts index dd33007..54a2ff0 100644 --- a/migrations/1660540253547-createNotificationType.ts +++ b/migrations/1660540253547-createNotificationType.ts @@ -37,6 +37,30 @@ export class createNotificationType1660540253547 implements MigrationInterface { isNullable: false, default: false, }, + { + name: 'webDefaultValue', + type: 'boolean', + isNullable: false, + default: true, + }, + { + name: 'emailDefaultValue', + type: 'boolean', + isNullable: false, + default: false, + }, + { + name: 'isWebEditable', + type: 'boolean', + isNullable: false, + default: true, + }, + { + name: 'isEmailEditable', + type: 'boolean', + isNullable: false, + default: false, + }, { name: 'categoryGroup', type: 'text', diff --git a/migrations/1660716115917-seedNotificationType.ts b/migrations/1660716115917-seedNotificationType.ts index 3c6821c..bc1f315 100644 --- a/migrations/1660716115917-seedNotificationType.ts +++ b/migrations/1660716115917-seedNotificationType.ts @@ -164,6 +164,7 @@ export const GivethNotificationTypes = { title: 'Project published', description: 'When Project has been published', showOnSettingPage: true, + isEmailEditable: true, category: NOTIFICATION_CATEGORY.PROJECT_RELATED, icon: 'IconPublish', microService: MICRO_SERVICES.givethio, @@ -1756,22 +1757,21 @@ export const GivethNotificationTypes = { PROJECT_STATUS_GROUP: { name: 'Project status', title: 'Project status', - description: - 'Your project status', + description: 'Your project status', showOnSettingPage: true, + isEmailEditable: true, + isWebEditable: false, microService: MICRO_SERVICES.givethio, schemaValidator: null, emailNotifierService: null, emailNotificationId: null, pushNotifierService: null, isGroupParent: true, - content: - 'Your project status', + content: 'Your project status', htmlTemplate: [ { type: 'p', - content: - 'Your project status', + content: 'Your project status', }, ], category: NOTIFICATION_CATEGORY.PROJECT_RELATED, @@ -1784,6 +1784,7 @@ export const GivethNotificationTypes = { description: 'When someone donates to your project, when you donate to a project, donation success and failure.', showOnSettingPage: true, + isEmailEditable: true, microService: MICRO_SERVICES.givethio, schemaValidator: null, emailNotifierService: null, @@ -1853,8 +1854,7 @@ export const GivethNotificationTypes = { LIKED_BY_YOU_PROJECT_GROUP: { name: 'Project status - Users Who Liked', title: 'Project status', - description: - 'Your liked project status', + description: 'Your liked project status', showOnSettingPage: true, microService: MICRO_SERVICES.givethio, schemaValidator: null, @@ -1862,13 +1862,11 @@ export const GivethNotificationTypes = { emailNotificationId: null, pushNotifierService: null, isGroupParent: true, - content: - 'Your liked project status', + content: 'Your liked project status', htmlTemplate: [ { type: 'p', - content: - 'Your liked project status', + content: 'Your liked project status', }, ], category: NOTIFICATION_CATEGORY.PROJECT_RELATED, diff --git a/src/controllers/v1/notificationSettingsController.ts b/src/controllers/v1/notificationSettingsController.ts index 7dd480a..5c35320 100644 --- a/src/controllers/v1/notificationSettingsController.ts +++ b/src/controllers/v1/notificationSettingsController.ts @@ -1,16 +1,23 @@ import { Route, Tags, Body, Security, Inject, Get, Query, Put } from 'tsoa'; import { logger } from '../../utils/logger'; import { + findNotificationSettingById, getUserNotificationSettings, updateUserNotificationSetting, } from '../../repositories/notificationSettingRepository'; import { UserAddress } from '../../entities/userAddress'; +import { errorMessages } from '../../utils/errorMessages'; +import { + sendNotificationValidator, + updateNotificationSettings, + updateOneNotificationSetting, + validateWithJoiSchema, +} from '../../validators/schemaValidators'; interface SettingParams { id: number; - allowNotifications?: string; - allowEmailNotification?: string; - allowDappPushNotification?: string; + allowEmailNotification: boolean; + allowDappPushNotification: boolean; } @Route('/v1/notification_settings') @@ -32,19 +39,16 @@ export class NotificationSettingsController { const { settings } = body; try { + validateWithJoiSchema(body, updateNotificationSettings); + const updatedNotifications = await Promise.all( settings.map(async setting => { - const { - id, - allowNotifications, - allowEmailNotification, - allowDappPushNotification, - } = setting; + const { id, allowEmailNotification, allowDappPushNotification } = + setting; const updatedNotification = await updateUserNotificationSetting({ notificationSettingId: id, userAddressId: user.id, - allowNotifications, allowEmailNotification, allowDappPushNotification, }); @@ -66,9 +70,8 @@ export class NotificationSettingsController { @Body() body: { id: number; - allowNotifications?: string; - allowEmailNotification?: string; - allowDappPushNotification?: string; + allowEmailNotification: boolean; + allowDappPushNotification: boolean; }, @Inject() params: { @@ -76,22 +79,22 @@ export class NotificationSettingsController { }, ) { const { user } = params; - const { - id, - allowNotifications, - allowEmailNotification, - allowDappPushNotification, - } = body; + const { id, allowEmailNotification, allowDappPushNotification } = body; try { - const updatedNotification = await updateUserNotificationSetting({ + validateWithJoiSchema(body, updateOneNotificationSetting); + + const notificationSetting = await findNotificationSettingById(id); + + if (!notificationSetting) { + throw new Error(errorMessages.NOTIFICATION_SETTING_NOT_FOUND); + } + const newSettingData = { notificationSettingId: id, userAddressId: user.id, - allowNotifications, allowEmailNotification, allowDappPushNotification, - }); - - return updatedNotification; + }; + return await updateUserNotificationSetting(newSettingData); } catch (e) { logger.error('updateNotificationSetting() error', e); throw e; diff --git a/src/entities/notificationType.ts b/src/entities/notificationType.ts index 6afe285..fce53ab 100644 --- a/src/entities/notificationType.ts +++ b/src/entities/notificationType.ts @@ -83,6 +83,19 @@ export class NotificationType extends BaseEntity { @Column('boolean', { default: false }) showOnSettingPage?: boolean; + // https://github.com/Giveth/impact-graph/issues/746 + @Column('boolean', { nullable: true, default: false }) + isEmailEditable: boolean; + + @Column('boolean', { nullable: true, default: true }) + isWebEditable: boolean; + + @Column('boolean', { nullable: true, default: false }) + emailDefaultValue: boolean; + + @Column('boolean', { nullable: true, default: false }) + webDefaultValue: boolean; + @Index() @Column('text', { nullable: true }) categoryGroup?: string; diff --git a/src/repositories/notificationSettingRepository.test.ts b/src/repositories/notificationSettingRepository.test.ts index e04d4d5..3647fcd 100644 --- a/src/repositories/notificationSettingRepository.test.ts +++ b/src/repositories/notificationSettingRepository.test.ts @@ -103,6 +103,8 @@ function updateUserNotificationSettingTestCases() { userAddressId: userAddress.id, }) .andWhere('notificationType.isGroupParent = false') + .andWhere('notificationType.isEmailEditable = true') + .andWhere('notificationType.isWebEditable = true') .getOne(); assert.isTrue(userNotificationSetting?.userAddressId === userAddress.id); @@ -110,19 +112,60 @@ function updateUserNotificationSettingTestCases() { const updatedSetting = await updateUserNotificationSetting({ notificationSettingId: userNotificationSetting!.id, userAddressId: userAddress.id, - allowNotifications: 'false', - allowEmailNotification: 'false', - allowDappPushNotification: 'false', + allowEmailNotification: false, + allowDappPushNotification: false, }); + assert.isOk(updatedSetting); + assert.isFalse(updatedSetting?.allowEmailNotification); + assert.isFalse(updatedSetting?.allowDappPushNotification); + }); + it('update user notification settings, cant change when isEmailEditable is false', async () => { + const userAddress = await createNewUserAddressIfNotExists(walletAddress); + const userNotificationSetting = + await NotificationSetting.createQueryBuilder('setting') + .leftJoinAndSelect('setting.notificationType', 'notificationType') + .where('setting."userAddressId" = :userAddressId', { + userAddressId: userAddress.id, + }) + .andWhere('notificationType.isGroupParent = false') + .andWhere('notificationType.isEmailEditable = false') + .andWhere('notificationType.isWebEditable = true') + .getOne(); + + assert.isTrue(userNotificationSetting?.userAddressId === userAddress.id); + + const updatedSetting = await updateUserNotificationSetting({ + notificationSettingId: userNotificationSetting!.id, + userAddressId: userAddress.id, + allowEmailNotification: false, + allowDappPushNotification: false, + }); assert.isOk(updatedSetting); - assert.isTrue( - userNotificationSetting!.allowNotifications !== - updatedSetting!.allowNotifications, - ); - assert.equal(userNotificationSetting!.allowNotifications, true); - assert.equal(updatedSetting.allowNotifications, false); - assert.equal(updatedSetting.allowEmailNotification, false); - assert.equal(updatedSetting.allowDappPushNotification, false); + assert.isTrue(updatedSetting?.allowEmailNotification); + assert.isFalse(updatedSetting?.allowDappPushNotification); + }); + it('update user notification settings, cant change when isWebEditable is false', async () => { + const userAddress = await createNewUserAddressIfNotExists(walletAddress); + + const userNotificationSetting = + await NotificationSetting.createQueryBuilder('setting') + .leftJoinAndSelect('setting.notificationType', 'notificationType') + .where('setting."userAddressId" = :userAddressId', { + userAddressId: userAddress.id, + }) + .andWhere('notificationType.isWebEditable = false') + .getOne(); + + assert.isTrue(userNotificationSetting?.userAddressId === userAddress.id); + + const updatedSetting = await updateUserNotificationSetting({ + notificationSettingId: userNotificationSetting!.id, + userAddressId: userAddress.id, + allowEmailNotification: false, + allowDappPushNotification: false, + }); + assert.isOk(updatedSetting); + assert.isTrue(updatedSetting?.allowDappPushNotification); }); } diff --git a/src/repositories/notificationSettingRepository.ts b/src/repositories/notificationSettingRepository.ts index c423655..98feb94 100644 --- a/src/repositories/notificationSettingRepository.ts +++ b/src/repositories/notificationSettingRepository.ts @@ -76,13 +76,34 @@ export const findNotificationSettingByNotificationTypeAndUserAddress = } }; +export const findNotificationSettingById = async ( + id: number, +): Promise => { + try { + return await NotificationSetting.createQueryBuilder('notificationSetting') + .leftJoinAndSelect( + 'notificationSetting.notificationType', + 'notificationType', + ) + .where('notificationSetting.id = :id', { + id, + }) + .getOne(); + } catch (e) { + logger.error( + 'findNotificationSettingByNotificationTypeAndUserAddress() error', + e, + ); + throw e; + } +}; + export const updateUserNotificationSetting = async (params: { notificationSettingId: number; userAddressId: number; - allowNotifications?: string; - allowEmailNotification?: string; - allowDappPushNotification?: string; -}) => { + allowEmailNotification: boolean; + allowDappPushNotification: boolean; +}): Promise => { const notificationSetting = await NotificationSetting.createQueryBuilder( 'notificationSetting', ) @@ -102,15 +123,13 @@ export const updateUserNotificationSetting = async (params: { if (!notificationSetting) throw new Error(errorMessages.NOTIFICATION_SETTING_NOT_FOUND); - if (params.allowNotifications) - notificationSetting.allowNotifications = - params.allowNotifications === 'true'; - if (params.allowEmailNotification) - notificationSetting.allowEmailNotification = - params.allowEmailNotification === 'true'; - if (params.allowDappPushNotification) + if (notificationSetting.notificationType?.isEmailEditable) { + notificationSetting.allowEmailNotification = params.allowEmailNotification; + } + if (notificationSetting.notificationType?.isWebEditable) { notificationSetting.allowDappPushNotification = - params.allowDappPushNotification === 'true'; + params.allowDappPushNotification; + } if ( notificationSetting?.notificationType?.isGroupParent && @@ -119,7 +138,6 @@ export const updateUserNotificationSetting = async (params: { await updateChildNotificationSettings({ categoryGroup: notificationSetting?.notificationType?.categoryGroup, userAddressId: params.userAddressId, - allowNotifications: notificationSetting.allowNotifications, allowEmailNotification: notificationSetting.allowEmailNotification, allowDappPushNotification: notificationSetting.allowDappPushNotification, }); @@ -131,9 +149,8 @@ export const updateUserNotificationSetting = async (params: { export const updateChildNotificationSettings = async (params: { categoryGroup: string; userAddressId: number; - allowNotifications?: boolean; - allowEmailNotification?: boolean; - allowDappPushNotification?: boolean; + allowEmailNotification: boolean; + allowDappPushNotification: boolean; }) => { // Grab type ids const notificationTypes = await NotificationType.createQueryBuilder( @@ -153,14 +170,12 @@ export const updateChildNotificationSettings = async (params: { if (notificationTypeIds.length === 0) return; await NotificationSetting.query(` - UPDATE notification_setting - SET "allowNotifications" = ${ - params.allowNotifications - }, "allowEmailNotification" = ${ - params.allowEmailNotification - }, "allowDappPushNotification" = ${params.allowDappPushNotification} - WHERE "notificationTypeId" IN (${notificationTypeIds.join( - ',', - )}) AND "userAddressId" = ${params.userAddressId} + UPDATE notification_setting + SET "allowEmailNotification" = ${ + params.allowEmailNotification + }, "allowDappPushNotification" = ${params.allowDappPushNotification} + WHERE "notificationTypeId" IN (${notificationTypeIds.join( + ',', + )}) AND "userAddressId" = ${params.userAddressId} `); }; diff --git a/src/routes/v1/notificationSettingsRouter.test.ts b/src/routes/v1/notificationSettingsRouter.test.ts index 17554fd..b57d799 100644 --- a/src/routes/v1/notificationSettingsRouter.test.ts +++ b/src/routes/v1/notificationSettingsRouter.test.ts @@ -11,6 +11,7 @@ import { NOTIFICATION_CATEGORY_GROUPS, } from '../../entities/notificationSetting'; import { NOTIFICATION_CATEGORY } from '../../types/general'; +import { not } from 'joi'; const apiBaseUrl = serverUrl; @@ -70,41 +71,25 @@ function updateNotificationsTestCases() { 'notificationType', ) .where('notificationSetting.userAddressId = :id', { id: userAddress.id }) - .andWhere( - 'notificationType.isGroupParent = true AND notificationType.categoryGroup = :categoryGroup', - { categoryGroup: NOTIFICATION_CATEGORY_GROUPS.GIVPOWER_ALLOCATIONS }, - ) + .andWhere('notificationType.isEmailEditable = true') + .andWhere('notificationType.isWebEditable = true') .getOne(); const jwtToken = jwt.sign({ publicAddress: walletAddress }, 'xxxx'); const result = await Axios.put( - `${apiBaseUrl}/v1/notification_settings/${notificationSetting!.id}`, + `${apiBaseUrl}/v1/notification_settings/${notificationSetting?.id}`, { id: notificationSetting!.id, - allowEmailNotification: 'false', - allowDappPushNotification: 'false', + allowEmailNotification: false, + allowDappPushNotification: false, }, { headers: { Authorization: `Bearer ${jwtToken}` } }, ); const updatedNotification = result.data; assert.isOk(result); - // didnt update this value so it remains true - assert.isTrue(updatedNotification.allowNotifications === true); - assert.isTrue( - updatedNotification.allowNotifications === - notificationSetting?.allowNotifications, - ); - assert.isTrue(updatedNotification.allowEmailNotification === false); - assert.isTrue( - updatedNotification.allowEmailNotification !== - notificationSetting?.allowEmailNotification, - ); - assert.isTrue(updatedNotification.allowDappPushNotification === false); - assert.isTrue( - updatedNotification.allowDappPushNotification !== - notificationSetting?.allowDappPushNotification, - ); + assert.isFalse(updatedNotification.allowEmailNotification); + assert.isFalse(updatedNotification.allowDappPushNotification); //validate child notifications of the group are updated const updatedChildSettings = await NotificationSetting.createQueryBuilder( 'notificationSetting', @@ -121,11 +106,69 @@ function updateNotificationsTestCases() { .getMany(); updatedChildSettings.forEach(setting => { - assert.isTrue(setting.allowNotifications === true); - assert.isTrue(setting.allowEmailNotification === false); - assert.isTrue(setting.allowDappPushNotification === false); + assert.isTrue(setting.allowNotifications); + assert.isTrue(setting.allowEmailNotification); + assert.isTrue(setting.allowDappPushNotification); }); }); + it('should update notification setting, when isEmailEditable is false should not change email setting', async () => { + const userAddress = await createNewUserAddressIfNotExists(walletAddress); + const notificationSetting = await NotificationSetting.createQueryBuilder( + 'notificationSetting', + ) + .leftJoinAndSelect( + 'notificationSetting.notificationType', + 'notificationType', + ) + .where('notificationSetting.userAddressId = :id', { id: userAddress.id }) + .andWhere('notificationType.isEmailEditable = false') + .andWhere('notificationSetting.allowDappPushNotification = true') + .getOne(); + const jwtToken = jwt.sign({ publicAddress: walletAddress }, 'xxxx'); + const result = await Axios.put( + `${apiBaseUrl}/v1/notification_settings/${notificationSetting?.id}`, + { + id: notificationSetting!.id, + allowEmailNotification: false, + allowDappPushNotification: false, + }, + { headers: { Authorization: `Bearer ${jwtToken}` } }, + ); + + const updatedNotification = result.data; + assert.isOk(result); + + assert.isTrue(updatedNotification.allowEmailNotification); + }); + it('should update notification setting, when isWebEditable is false should not change dapp push notification setting', async () => { + const userAddress = await createNewUserAddressIfNotExists(walletAddress); + const notificationSetting = await NotificationSetting.createQueryBuilder( + 'notificationSetting', + ) + .leftJoinAndSelect( + 'notificationSetting.notificationType', + 'notificationType', + ) + .where('notificationSetting.userAddressId = :id', { id: userAddress.id }) + .andWhere('notificationType.isWebEditable = false') + .andWhere('notificationSetting.allowDappPushNotification = true') + .getOne(); + const jwtToken = jwt.sign({ publicAddress: walletAddress }, 'xxxx'); + const result = await Axios.put( + `${apiBaseUrl}/v1/notification_settings/${notificationSetting?.id}`, + { + id: notificationSetting!.id, + allowEmailNotification: false, + allowDappPushNotification: false, + }, + { headers: { Authorization: `Bearer ${jwtToken}` } }, + ); + + const updatedNotification = result.data; + assert.isOk(result); + + assert.isTrue(updatedNotification.allowDappPushNotification); + }); } function updateMultipleNotificationsTestCases() { @@ -134,7 +177,13 @@ function updateMultipleNotificationsTestCases() { const notificationSettings = await NotificationSetting.createQueryBuilder( 'notificationSetting', ) + .leftJoinAndSelect( + 'notificationSetting.notificationType', + 'notificationType', + ) .where('notificationSetting.userAddressId = :id', { id: userAddress.id }) + .andWhere('notificationType.isEmailEditable = true') + .andWhere('notificationType.isWebEditable = true') .take(2) .getMany(); @@ -145,8 +194,8 @@ function updateMultipleNotificationsTestCases() { settings: notificationSettings.map(setting => { return { id: setting!.id, - allowEmailNotification: 'false', - allowDappPushNotification: 'false', + allowEmailNotification: false, + allowDappPushNotification: false, }; }), }, @@ -156,8 +205,8 @@ function updateMultipleNotificationsTestCases() { const updatedNotifications: any[] = Object.values(result.data); assert.isOk(result); // didnt update this value so it remains true - assert.isTrue(updatedNotifications[0].allowNotifications === true); - assert.isTrue(updatedNotifications[1].allowNotifications === true); + assert.isTrue(updatedNotifications[0].allowNotifications); + assert.isTrue(updatedNotifications[1].allowNotifications); const updatedNotification1 = updatedNotifications.find((setting: any) => { return setting.id === notificationSettings[0].id; @@ -172,12 +221,12 @@ function updateMultipleNotificationsTestCases() { updatedNotification1.allowNotifications === notificationSettings[0]?.allowNotifications, ); - assert.isTrue(updatedNotification1.allowEmailNotification === false); + assert.isFalse(updatedNotification1.allowEmailNotification); assert.isTrue( updatedNotification1.allowEmailNotification !== notificationSettings[0]?.allowEmailNotification, ); - assert.isTrue(updatedNotification1.allowDappPushNotification === false); + assert.isFalse(updatedNotification1.allowDappPushNotification); assert.isTrue( updatedNotification1.allowDappPushNotification !== notificationSettings[0]?.allowDappPushNotification, @@ -188,12 +237,12 @@ function updateMultipleNotificationsTestCases() { updatedNotification2.allowNotifications === notificationSettings[1]?.allowNotifications, ); - assert.isTrue(updatedNotification2.allowEmailNotification === false); + assert.isFalse(updatedNotification2.allowEmailNotification); assert.isTrue( updatedNotification2.allowEmailNotification !== notificationSettings[1]?.allowEmailNotification, ); - assert.isTrue(updatedNotification2.allowDappPushNotification === false); + assert.isFalse(updatedNotification2.allowDappPushNotification); assert.isTrue( updatedNotification2.allowDappPushNotification !== notificationSettings[1]?.allowDappPushNotification, diff --git a/src/utils/validators/segmentAndMetadataValidators.ts b/src/utils/validators/segmentAndMetadataValidators.ts index d302ce9..4915221 100644 --- a/src/utils/validators/segmentAndMetadataValidators.ts +++ b/src/utils/validators/segmentAndMetadataValidators.ts @@ -21,14 +21,13 @@ const projectRelatedTrackerSchema = Joi.object({ // seems we have to different schemas, not good TODO CHANGE ON IMPACT GRAPH email: Joi.string(), title: Joi.string().required(), - firstName: Joi.string().allow(null,''), - lastName: Joi.string().allow(null,''), + firstName: Joi.string().allow(null, ''), + lastName: Joi.string().allow(null, ''), OwnerId: Joi.number(), slug: Joi.string().required(), // it's for project updates - update: Joi.string().allow(null,''), - + update: Joi.string().allow(null, ''), }); const boostedSchema = Joi.object({ diff --git a/src/validators/schemaValidators.ts b/src/validators/schemaValidators.ts index c8ef761..dcd6536 100644 --- a/src/validators/schemaValidators.ts +++ b/src/validators/schemaValidators.ts @@ -73,6 +73,23 @@ export const sendNotificationValidator = Joi.object({ }), }); +export const updateNotificationSettings = Joi.object({ + settings: Joi.array() + .required() + .items( + Joi.object({ + id: Joi.number().required(), + allowEmailNotification: Joi.boolean().required(), + allowDappPushNotification: Joi.boolean().required(), + }), + ), +}); +export const updateOneNotificationSetting = Joi.object({ + id: Joi.number().required(), + allowEmailNotification: Joi.boolean().required(), + allowDappPushNotification: Joi.boolean().required(), +}); + export const getNotificationsValidator = Joi.object({ // TODO should fill it });