diff --git a/migrations/1660539566228-createUserAddress.ts b/migrations/1660539566228-createUserAddress.ts index 464d949..1c11c49 100644 --- a/migrations/1660539566228-createUserAddress.ts +++ b/migrations/1660539566228-createUserAddress.ts @@ -1,9 +1,4 @@ -import { - MigrationInterface, - QueryRunner, - Table, - TableForeignKey, -} from 'typeorm'; +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; export class createUserAddress1660539566228 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { diff --git a/migrations/1660540253547-createNotificationType.ts b/migrations/1660540253547-createNotificationType.ts index 54a2ff0..b4d1fb6 100644 --- a/migrations/1660540253547-createNotificationType.ts +++ b/migrations/1660540253547-createNotificationType.ts @@ -1,10 +1,4 @@ -import { - MigrationInterface, - QueryRunner, - Table, - TableForeignKey, - TableIndex, -} from 'typeorm'; +import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm'; export class createNotificationType1660540253547 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { diff --git a/migrations/1660541108096-createNotificationSetting.ts b/migrations/1660541108096-createNotificationSetting.ts index 0825dd5..5c0a909 100644 --- a/migrations/1660541108096-createNotificationSetting.ts +++ b/migrations/1660541108096-createNotificationSetting.ts @@ -42,7 +42,7 @@ export class createNotificationSetting1660541108096 { name: 'notificationTypeId', type: 'int', - isNullable: true, + isNullable: false, }, { name: 'userAddressId', diff --git a/migrations/1660716115917-seedNotificationType.ts b/migrations/1660716115917-seedNotificationType.ts index 605b069..fd80bf3 100644 --- a/migrations/1660716115917-seedNotificationType.ts +++ b/migrations/1660716115917-seedNotificationType.ts @@ -320,6 +320,29 @@ export const GivethNotificationTypes = { content: 'The {project name} that you donated before is no longer visible.\n{reason}', }, + PROJECT_BOOSTED: { + name: 'Project boosted', + description: 'Project has been boosted', + microService: MICRO_SERVICES.givethio, + category: NOTIFICATION_CATEGORY.PROJECT_RELATED, + icon: '', + schemaValidator: SCHEMA_VALIDATORS_NAMES.PROJECT_BOOSTED, + emailNotifierService: THIRD_PARTY_EMAIL_SERVICES.SEGMENT, + emailNotificationId: null, + pushNotifierService: null, + htmlTemplate: [ + { + type: 'p', + content: 'Someone boosted your project ', + }, + { + type: 'a', + content: '$projectTitle', + href: '$projectLink', + }, + ], + content: 'Someone boosted your project {projectName}!', + }, PROJECT_CANCELLED: { name: 'Project cancelled', description: 'Project has been cancelled', @@ -873,6 +896,10 @@ export const GivethNotificationTypes = { pushNotifierService: null, categoryGroup: NOTIFICATION_CATEGORY_GROUPS.STAKING, title: 'Stake', + isEmailEditable: false, + isWebEditable: false, + emailDefaultValue: false, + webDefaultValue: false, htmlTemplate: [ { type: 'p', @@ -888,7 +915,7 @@ export const GivethNotificationTypes = { }, { type: 'a', - content: '$poolName', + content: '$contractName', href: '/givfarm', }, { @@ -896,7 +923,7 @@ export const GivethNotificationTypes = { content: ' was successful.', }, ], - content: 'Staking {amount} of {poolName} was successful.', + content: 'Staking {amount} of {contractName} was successful.', }, UN_STAKE: { name: 'UnStake', @@ -909,6 +936,10 @@ export const GivethNotificationTypes = { emailNotificationId: null, pushNotifierService: null, categoryGroup: NOTIFICATION_CATEGORY_GROUPS.STAKING, + isEmailEditable: false, + isWebEditable: false, + emailDefaultValue: false, + webDefaultValue: false, title: 'UnStake', htmlTemplate: [ { @@ -925,7 +956,7 @@ export const GivethNotificationTypes = { }, { type: 'a', - content: '$poolName', + content: '$contractName', href: '/givfarm', }, { @@ -933,32 +964,32 @@ export const GivethNotificationTypes = { content: ' was successful.', }, ], - content: 'Unstaking {amount} of {poolname} was successful.', + content: 'Unstaking {amount} of {contractName} was successful.', }, - GIV_BACKS_ARE_READY_TO_CLAIM: { - name: 'GIVbacks are ready to claim', - title: 'GIVbacks', - description: 'When GIVbacks are ready to be claimed after each round', + GIV_BACK_IS_READY_TO_CLAIM: { + name: 'GIVback is ready to claim', + title: 'GIVback', + description: 'When GIVback is ready to be claimed after each round', showOnSettingPage: true, - microService: MICRO_SERVICES.givethio, + microService: MICRO_SERVICES.givEconomyNotificationMicroService, category: NOTIFICATION_CATEGORY.GIV_ECONOMY, - icon: 'IconUnstake', - schemaValidator: SCHEMA_VALIDATORS_NAMES.GIV_FARM_READY_TO_CLAIM, - emailNotifierService: THIRD_PARTY_EMAIL_SERVICES.SEGMENT, - emailNotificationId: SegmentEvents.PROJECT_REJECTED, + icon: 'IconGIVBack', + schemaValidator: SCHEMA_VALIDATORS_NAMES.GIV_BACK_IS_READY_TO_CLAIM, + emailNotifierService: null, + emailNotificationId: null, pushNotifierService: null, htmlTemplate: [ { type: 'p', - content: 'Your GIVbacks for round ', + content: 'Your GIVback ', }, { type: 'b', - content: '$round', + content: '$amount', }, { type: 'p', - content: ' are ready to claim! ', + content: ' GIV is ready to claim! ', }, { type: 'br' }, { @@ -972,7 +1003,7 @@ export const GivethNotificationTypes = { }, ], content: - 'Your GIVbacks for round {round number} are ready to claim! \n{Click here} to take a shortcut.', + 'Your GIVback {amount} GIV is ready to claim! \n[Click here] to take a shortcut.', }, PROJECT_EDITED: { name: 'Project edited', @@ -1379,8 +1410,8 @@ export const GivethNotificationTypes = { PROJECT_UPDATED_OWNER: { name: 'Project updated - owner', - title: 'Project updates', - description: 'Your project update', + title: 'Your project update', + description: 'You Posted an update on your project', showOnSettingPage: true, microService: MICRO_SERVICES.givethio, category: NOTIFICATION_CATEGORY.PROJECT_RELATED, @@ -1483,8 +1514,8 @@ export const GivethNotificationTypes = { }, NEW_PROJECT_UPDATE_FOR_USERS_WHO_LIKED: { name: 'Project updates', - title: 'Project updates', - description: 'Your liked project update', + title: 'Your liked project update', + description: 'When your liked project has an update', showOnSettingPage: true, microService: MICRO_SERVICES.givethio, category: NOTIFICATION_CATEGORY.PROJECT_RELATED, @@ -1511,7 +1542,7 @@ export const GivethNotificationTypes = { name: 'You boosted', description: 'User boosted a project', microService: MICRO_SERVICES.givethio, - category: NOTIFICATION_CATEGORY.GIV_POWER, + category: NOTIFICATION_CATEGORY.GIV_ECONOMY, icon: '', schemaValidator: SCHEMA_VALIDATORS_NAMES.USER_BOOSTED, emailNotifierService: null, @@ -1548,7 +1579,7 @@ export const GivethNotificationTypes = { name: 'You changed the allocation', description: 'User changed boosted allocation', microService: MICRO_SERVICES.givethio, - category: NOTIFICATION_CATEGORY.GIV_POWER, + category: NOTIFICATION_CATEGORY.GIV_ECONOMY, icon: '', schemaValidator: SCHEMA_VALIDATORS_NAMES.USER_CHANGED_BOOSTED_ALLOCATION, emailNotifierService: null, @@ -1568,7 +1599,7 @@ export const GivethNotificationTypes = { name: 'Your project has been boosted.', description: 'Project has received a boosting', microService: MICRO_SERVICES.givethio, - category: NOTIFICATION_CATEGORY.GIV_POWER, + category: NOTIFICATION_CATEGORY.GIV_ECONOMY, icon: '', schemaValidator: SCHEMA_VALIDATORS_NAMES.PROJECT_HAS_BEEN_BOOSTED, emailNotifierService: null, @@ -1601,7 +1632,7 @@ export const GivethNotificationTypes = { name: 'You locked {amount} & recieved {amount} GIVpower', description: 'User locked GIVpower', microService: MICRO_SERVICES.givethio, - category: NOTIFICATION_CATEGORY.GIV_POWER, + category: NOTIFICATION_CATEGORY.GIV_ECONOMY, icon: '', schemaValidator: SCHEMA_VALIDATORS_NAMES.USER_LOCKED_GIVPOWER, emailNotifierService: null, @@ -1634,10 +1665,10 @@ export const GivethNotificationTypes = { content: 'You locked {amount} & recieved {amount} GIVpower', }, YOU_UNLOCKED_GIVPOWER: { - name: '{amount} unlocked.', + name: 'givPower unlocked', description: 'User unlocked GIVpower', - microService: MICRO_SERVICES.givethio, - category: NOTIFICATION_CATEGORY.GIV_POWER, + microService: MICRO_SERVICES.givEconomyNotificationMicroService, + category: NOTIFICATION_CATEGORY.GIV_ECONOMY, icon: '', schemaValidator: SCHEMA_VALIDATORS_NAMES.USER_UNLOCKED_GIVPOWER, emailNotifierService: null, @@ -1661,7 +1692,7 @@ export const GivethNotificationTypes = { name: '{amount} re-locked automatically', description: 'User unlocked GIVpower', microService: MICRO_SERVICES.givethio, - category: NOTIFICATION_CATEGORY.GIV_POWER, + category: NOTIFICATION_CATEGORY.GIV_ECONOMY, icon: '', schemaValidator: SCHEMA_VALIDATORS_NAMES.USER_GIVPOWER_RELOCKED_AUTOMATICALLY, @@ -1712,7 +1743,7 @@ export const GivethNotificationTypes = { title: 'Your boost status', description: 'Shows when you boost a project, change the allocation of GIVpower.', - showOnSettingPage: true, + showOnSettingPage: false, microService: MICRO_SERVICES.givethio, schemaValidator: null, emailNotifierService: null, @@ -1756,8 +1787,9 @@ export const GivethNotificationTypes = { }, PROJECT_STATUS_GROUP: { name: 'Project status', - title: 'Project status', - description: 'Your project status', + title: 'Your project status', + description: + 'When your own project has been listed, unlisted, cancelled, activated or deactivated', showOnSettingPage: true, isEmailEditable: true, isWebEditable: false, @@ -1809,7 +1841,7 @@ export const GivethNotificationTypes = { microService: MICRO_SERVICES.givethio, title: 'Stakes', description: 'Shows when you stake or unstake on the GIVfarm', - showOnSettingPage: true, + showOnSettingPage: false, schemaValidator: null, emailNotifierService: null, emailNotificationId: null, @@ -1853,8 +1885,9 @@ export const GivethNotificationTypes = { }, LIKED_BY_YOU_PROJECT_GROUP: { name: 'Project status - Users Who Liked', - title: 'Project status', - description: 'Your liked project status', + title: 'Your liked project status', + description: + 'When your liked Project has been listed, unlisted, cancelled, activated or deactivated', showOnSettingPage: true, microService: MICRO_SERVICES.givethio, schemaValidator: null, @@ -1873,6 +1906,28 @@ export const GivethNotificationTypes = { icon: '', categoryGroup: NOTIFICATION_CATEGORY_GROUPS.LIKED_BY_YOU_PROJECT_GROUP, }, + + RAW_HTML: { + name: 'Raw HTML Broadcast', + description: 'Raw HTML Broadcast', + microService: MICRO_SERVICES.givethio, + category: 'general', + icon: 'IconAdminNotif', + schemaValidator: SCHEMA_VALIDATORS_NAMES.RAW_HTML_BROADCAST, + emailNotifierService: null, + emailNotificationId: null, + pushNotifierService: null, + title: 'Raw HTML Broadcast', + htmlTemplate: [ + { + type: 'html', + content: '$html', + }, + ], + Content: { + html: '', + }, + }, }; export class seedNotificationType1660716115917 implements MigrationInterface { diff --git a/src/adapters/jwtAuthentication/mockJwtAdapter.ts b/src/adapters/jwtAuthentication/mockJwtAdapter.ts index ea71c82..8fe38f4 100644 --- a/src/adapters/jwtAuthentication/mockJwtAdapter.ts +++ b/src/adapters/jwtAuthentication/mockJwtAdapter.ts @@ -2,13 +2,17 @@ import { JwtAuthenticationInterface } from './JwtAuthenticationInterface'; import { decode, JwtPayload } from 'jsonwebtoken'; import { errorMessages } from '../../utils/errorMessages'; import { logger } from '../../utils/logger'; +import { StandardError } from '../../types/StandardError'; export class MockJwtAdapter implements JwtAuthenticationInterface { verifyJwt(token: string): Promise { try { const decodedJwt = decode(token) as JwtPayload; if (!decodedJwt.publicAddress) { - throw new Error(errorMessages.UN_AUTHORIZED); + throw new StandardError({ + message: errorMessages.UN_AUTHORIZED, + httpStatusCode: 401, + }); } return decodedJwt.publicAddress.toLowerCase(); } catch (e: any) { diff --git a/src/controllers/v1/notificationSettingsController.ts b/src/controllers/v1/notificationSettingsController.ts index 5c35320..7d7ab3a 100644 --- a/src/controllers/v1/notificationSettingsController.ts +++ b/src/controllers/v1/notificationSettingsController.ts @@ -8,11 +8,11 @@ import { import { UserAddress } from '../../entities/userAddress'; import { errorMessages } from '../../utils/errorMessages'; import { - sendNotificationValidator, updateNotificationSettings, updateOneNotificationSetting, validateWithJoiSchema, } from '../../validators/schemaValidators'; +import { StandardError } from '../../types/StandardError'; interface SettingParams { id: number; @@ -86,7 +86,10 @@ export class NotificationSettingsController { const notificationSetting = await findNotificationSettingById(id); if (!notificationSetting) { - throw new Error(errorMessages.NOTIFICATION_SETTING_NOT_FOUND); + throw new StandardError({ + message: errorMessages.NOTIFICATION_SETTING_NOT_FOUND, + httpStatusCode: 400, + }); } const newSettingData = { notificationSettingId: id, diff --git a/src/controllers/v1/notificationsController.ts b/src/controllers/v1/notificationsController.ts index c877a49..cd4936a 100644 --- a/src/controllers/v1/notificationsController.ts +++ b/src/controllers/v1/notificationsController.ts @@ -9,12 +9,12 @@ import { Query, Put, Path, - Example, } from 'tsoa'; import { logger } from '../../utils/logger'; import { countUnreadValidator, + sendBulkNotificationValidator, sendNotificationValidator, validateWithJoiSchema, } from '../../validators/schemaValidators'; @@ -22,33 +22,23 @@ import { CountUnreadNotificationsResponse, GetNotificationsResponse, ReadAllNotificationsRequestType, - ReadAllNotificationsResponse, ReadSingleNotificationResponse, - SendNotificationRequest, SendNotificationResponse, - SendNotificationTypeRequest, + SendNotificationRequest, + sendBulkNotificationRequest, } from '../../types/requestResponses'; -import { errorMessages, errorMessagesEnum } from '../../utils/errorMessages'; -import { StandardError } from '../../types/StandardError'; -import { User } from '../../types/general'; +import { errorMessages } from '../../utils/errorMessages'; import { countUnreadNotifications, - createNotification, - findNotificationByTrackId, getNotifications, markNotificationGroupAsRead, markNotificationsAsRead, } from '../../repositories/notificationRepository'; import { UserAddress } from '../../entities/userAddress'; -import { - getNotificationTypeByEventName, - getNotificationTypeByEventNameAndMicroservice, -} from '../../repositories/notificationTypeRepository'; -import { EMAIL_STATUSES, Notification } from '../../entities/notification'; +import { Notification } from '../../entities/notification'; import { createNewUserAddressIfNotExists } from '../../repositories/userAddressRepository'; -import { SEGMENT_METADATA_SCHEMA_VALIDATOR } from '../../utils/validators/segmentAndMetadataValidators'; -import { findNotificationSettingByNotificationTypeAndUserAddress } from '../../repositories/notificationSettingRepository'; -import { SegmentAnalyticsSingleton } from '../../services/segment/segmentAnalyticsSingleton'; +import { sendNotification } from '../../services/notificationService'; +import { StandardError } from '../../types/StandardError'; @Route('/v1') @Tags('Notification') @@ -57,111 +47,56 @@ export class NotificationsController { @Security('basicAuth') public async sendNotification( @Body() - body: SendNotificationTypeRequest, + body: SendNotificationRequest, @Inject() params: { microService: string; }, ): Promise { const { microService } = params; - const { userWalletAddress, projectId } = body; try { validateWithJoiSchema(body, sendNotificationValidator); - if (body.trackId && (await findNotificationByTrackId(body.trackId))) { - // We dont throw error in this case but dont create new notification neither - return { - success: true, - message: errorMessages.DUPLICATED_TRACK_ID, - }; - } - const userAddress = await createNewUserAddressIfNotExists( - userWalletAddress as string, - ); - const notificationType = - await getNotificationTypeByEventNameAndMicroservice({ - eventName: body.eventName, - microService, - }); - - if (!notificationType) { - throw new Error(errorMessages.INVALID_NOTIFICATION_TYPE); - } - const notificationSetting = - await findNotificationSettingByNotificationTypeAndUserAddress({ - notificationTypeId: notificationType.id, - userAddressId: userAddress.id, - }); - - logger.debug('notificationController.sendNotification()', { - notificationSetting, - notificationType, + return sendNotification(body, microService); + // add if and logic for push notification (not in mvp) + } catch (e) { + logger.error('sendNotification() error', { + error: e, + requestBody: body, + segmentPayload: body?.segment?.payload, }); + throw e; + } + } - const shouldSendEmail = - body.sendEmail && notificationSetting?.allowEmailNotification; - let emailStatus = shouldSendEmail - ? EMAIL_STATUSES.WAITING_TO_BE_SEND - : EMAIL_STATUSES.NO_NEED_TO_SEND; - - const segmentValidator = - SEGMENT_METADATA_SCHEMA_VALIDATOR[ - notificationType?.schemaValidator as string - ]?.segment; - - if (shouldSendEmail && body.sendSegment && segmentValidator) { - //TODO Currently sending email and segment event are tightly coupled, we can't send segment event without sending email - // And it's not good, we should find another solution to separate sending segment and email - const segmentData = body.segment?.payload; - validateWithJoiSchema(segmentData, segmentValidator); - await SegmentAnalyticsSingleton.getInstance().track({ - eventName: notificationType.emailNotificationId as string, - anonymousId: body?.segment?.anonymousId, - properties: segmentData, - analyticsUserId: body?.segment?.analyticsUserId, + @Post('/thirdParty/notificationsBulk') + @Security('basicAuth') + public async sendBulkNotification( + @Body() + body: sendBulkNotificationRequest, + @Inject() + params: { + microService: string; + }, + ): Promise { + const { microService } = params; + try { + validateWithJoiSchema(body, sendBulkNotificationValidator); + const trackIdsSet = new Set( + body.notifications.map(notification => notification.trackId), + ); + if (trackIdsSet.size !== body.notifications.length) { + throw new StandardError({ + message: errorMessages.THERE_IS_SOME_ITEMS_WITH_SAME_TRACK_ID, + httpStatusCode: 400, }); - emailStatus = EMAIL_STATUSES.SENT; - } - - const metadataValidator = - SEGMENT_METADATA_SCHEMA_VALIDATOR[ - notificationType?.schemaValidator as string - ]?.metadata; - - if (metadataValidator) { - validateWithJoiSchema(body.metadata, metadataValidator); - } - - if (!notificationSetting?.allowDappPushNotification) { - //TODO In future we can add a create notification but with disabledNotification:true - // So we can exclude them in list of notifications - return { - success: true, - message: errorMessages.USER_TURNED_OF_THIS_NOTIFICATION_TYPE, - }; } - const notificationData: Partial = { - notificationType, - userAddress, - email: body.email, - emailStatus, - trackId: body?.trackId, - metadata: body?.metadata, - segmentData: body.segment, - projectId, - }; - if (body.creationTime) { - // creationTime is optional and it's timestamp in milliseconds format - notificationData.createdAt = new Date(body.creationTime); - } - await createNotification(notificationData); - + await Promise.all( + body.notifications.map(item => sendNotification(item, microService)), + ); return { success: true }; - // add if and logic for push notification (not in mvp) } catch (e) { - logger.error('sendNotification() error', { + logger.error('sendBulkNotification() error', { error: e, - requestBody: body, - segmentPayload: body?.segment?.payload, }); throw e; } @@ -169,18 +104,18 @@ export class NotificationsController { // https://tsoa-community.github.io/docs/examples.html#parameter-examples /** - * @example limit "20" - * @example offset "0" - * @example category "" - * @example category "projectRelated" - * @example category "givEconomyRelated" - * @example category "general" - * @example isRead "" - * @example isRead "false" - * @example isRead "true" - * @example startTime "1659356987" - - */ + * @example limit "20" + * @example offset "0" + * @example category "" + * @example category "projectRelated" + * @example category "givEconomyRelated" + * @example category "general" + * @example isRead "" + * @example isRead "false" + * @example isRead "true" + * @example startTime "1659356987" + + */ @Get('/notifications/') @Security('JWT') public async getNotifications( @@ -258,7 +193,10 @@ export class NotificationsController { userAddressId: user.id, }); if (!notification) { - throw new Error(errorMessages.NOTIFICATION_NOT_FOUND); + throw new StandardError({ + message: errorMessages.NOTIFICATION_NOT_FOUND, + httpStatusCode: 404, + }); } return { notification, diff --git a/src/entities/notificationSetting.ts b/src/entities/notificationSetting.ts index b57ddbd..db92441 100644 --- a/src/entities/notificationSetting.ts +++ b/src/entities/notificationSetting.ts @@ -41,13 +41,13 @@ export class NotificationSetting extends BaseEntity { allowDappPushNotification: boolean; @Index() - @ManyToOne(type => NotificationType, { eager: true, nullable: true }) - notificationType?: NotificationType; + @ManyToOne(type => NotificationType, { eager: true }) + notificationType: NotificationType; @RelationId( (notificationSetting: NotificationSetting) => notificationSetting.notificationType, ) - notificationTypeId?: number | null; + notificationTypeId: number; @Index() @ManyToOne(type => UserAddress, { eager: true, nullable: false }) diff --git a/src/entities/notificationType.ts b/src/entities/notificationType.ts index fce53ab..4faa0d0 100644 --- a/src/entities/notificationType.ts +++ b/src/entities/notificationType.ts @@ -17,6 +17,7 @@ import { NotificationSetting } from './notificationSetting'; // Export Object with Schemas to N1 lookup export const SCHEMA_VALIDATORS_NAMES = { ADMIN_MESSAGE: 'adminMessage', + RAW_HTML_BROADCAST: 'rawHtmlBroadcast', DRAFTED_PROJECT_SAVED: 'draftedProjectSavedValidator', DRAFTED_PROJECT_ACTIVATED: 'draftedProjectPublishedValidator', PROJECT_LISTED: 'projectListed', @@ -33,6 +34,7 @@ export const SCHEMA_VALIDATORS_NAMES = { PROJECT_ACTIVATED: 'projectActivated', PROJECT_DEACTIVATED: 'projectDeactivated', PROJECT_CANCELLED: 'projectCancelled', + PROJECT_BOOSTED: 'projectBoosted', VERIFICATION_FORM_SENT: 'verificationFormSent', VERIFICATION_FORM_Reapply_Reminder: 'verificationFormReapplyReminder', VERIFICATION_FORM_REJECTED: 'verificationFormRejected', @@ -59,6 +61,7 @@ export const SCHEMA_VALIDATORS_NAMES = { USER_UNLOCKED_GIVPOWER: 'givPowerUserUnlockedGivPower', USER_GIVPOWER_RELOCKED_AUTOMATICALLY: 'givPowerUserGivPowerRelockedAutoMatically', + GIV_BACK_IS_READY_TO_CLAIM: 'givBackReadyToClaim', }; export type HtmlTemplate = { type: string; content: string; href?: string }[]; diff --git a/src/repositories/notificationSettingRepository.test.ts b/src/repositories/notificationSettingRepository.test.ts index 3647fcd..97aa7eb 100644 --- a/src/repositories/notificationSettingRepository.test.ts +++ b/src/repositories/notificationSettingRepository.test.ts @@ -45,6 +45,30 @@ function getUserNotificationSettingsTestCases() { assert.isTrue(setting?.notificationType?.showOnSettingPage); }); }); + it('return notification settings order by notificationTypeId', async () => { + const userAddress = await createNewUserAddressIfNotExists( + generateRandomEthereumAddress(), + ); + + const [notificationSettings] = await getUserNotificationSettings( + 100, + 0, + userAddress.id, + ); + + assert.isOk(notificationSettings); + notificationSettings.forEach(setting => { + assert.isTrue(setting!.userAddressId === userAddress.id); + assert.isTrue(setting?.notificationType?.showOnSettingPage); + }); + + for (let i = 1; i < notificationSettings.length; i++) { + assert.isTrue( + notificationSettings[i]?.notificationTypeId > + notificationSettings[i - 1]?.notificationTypeId, + ); + } + }); } function findNotificationSettingByNotificationTypeAndUserAddressTestCases() { diff --git a/src/repositories/notificationSettingRepository.ts b/src/repositories/notificationSettingRepository.ts index 98feb94..7835128 100644 --- a/src/repositories/notificationSettingRepository.ts +++ b/src/repositories/notificationSettingRepository.ts @@ -4,6 +4,7 @@ import { NotificationSetting } from '../entities/notificationSetting'; import { errorMessages } from '../utils/errorMessages'; import { createQueryBuilder } from 'typeorm'; import { logger } from '../utils/logger'; +import { StandardError } from '../types/StandardError'; export const createNotificationSettingsForNewUser = async ( user: UserAddress, @@ -45,7 +46,7 @@ export const getUserNotificationSettings = async ( category: category, }); } - + query.orderBy('notificationType.id', 'ASC'); return query.take(take).skip(skip).getManyAndCount(); }; @@ -120,9 +121,12 @@ export const updateUserNotificationSetting = async (params: { ) .getOne(); - if (!notificationSetting) - throw new Error(errorMessages.NOTIFICATION_SETTING_NOT_FOUND); - + if (!notificationSetting) { + throw new StandardError({ + message: errorMessages.NOTIFICATION_SETTING_NOT_FOUND, + httpStatusCode: 400, + }); + } if (notificationSetting.notificationType?.isEmailEditable) { notificationSetting.allowEmailNotification = params.allowEmailNotification; } diff --git a/src/routes/v1/notificationRouter.test.ts b/src/routes/v1/notificationRouter.test.ts index 8c9ea71..fb8bfe5 100644 --- a/src/routes/v1/notificationRouter.test.ts +++ b/src/routes/v1/notificationRouter.test.ts @@ -11,11 +11,14 @@ import axios from 'axios'; import { assert } from 'chai'; import { errorMessages, errorMessagesEnum } from '../../utils/errorMessages'; import { findNotificationByTrackId } from '../../repositories/notificationRepository'; +import { generateRandomString } from '../../utils/utils'; describe('/notifications POST test cases', sendNotificationTestCases); +describe('/notificationsBulk POST test cases', sendBulkNotificationsTestCases); describe('/notifications GET test cases', getNotificationTestCases); const sendNotificationUrl = `${serverUrl}/v1/thirdParty/notifications`; +const sendBulkNotificationsUrl = `${serverUrl}/v1/thirdParty/notificationsBulk`; const getNotificationUrl = `${serverUrl}/v1/notifications`; const projectTitle = 'project title'; const projectLink = 'https://giveth.io/project/project-title'; @@ -162,6 +165,52 @@ function sendNotificationTestCases() { } }); + it('should create *Raw HTML Broadcast* notification, success, segment is off', async () => { + const data = { + eventName: 'Raw HTML Broadcast', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + metadata: { + html: '

dasl;dksa;lkd;laskd;askd;alsdas hi

', + }, + }; + + const result = await axios.post(sendNotificationUrl, data, { + headers: { + authorization: getGivethIoBasicAuth(), + }, + }); + assert.equal(result.status, 200); + assert.isOk(result.data); + assert.isTrue(result.data.success); + }); + it('should create *Raw HTML Broadcast* notification, failed invalid metadata, segment is off', async () => { + try { + const data = { + eventName: 'Raw HTML Broadcast', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + metadata: {}, + }; + + await axios.post(sendNotificationUrl, data, { + headers: { + authorization: getGivethIoBasicAuth(), + }, + }); + // If request doesn't fail, it means this test failed + assert.isTrue(false); + } catch (e: any) { + assert.equal( + e.response.data.message, + errorMessagesEnum.IMPACT_GRAPH_VALIDATION_ERROR.message, + ); + assert.equal(e.response.data.description, '"html" is required'); + } + }); + it('should create *draft project saved* notification, success, segment is off', async () => { const data = { eventName: 'The project saved as draft', @@ -1274,7 +1323,7 @@ function sendNotificationTestCases() { sendSegment: false, userWalletAddress: generateRandomEthereumAddress(), metadata: { - poolName: 'GIV farm', + contractName: 'GIV farm', amount: '10', network: 100, transactionHash: generateRandomTxHash(), @@ -1298,7 +1347,7 @@ function sendNotificationTestCases() { sendSegment: false, userWalletAddress: generateRandomEthereumAddress(), metadata: { - poolName: 'GIV farm', + contractName: 'GIV farm', network: 100, transactionHash: generateRandomTxHash(), }, @@ -1320,6 +1369,61 @@ function sendNotificationTestCases() { } }); + it('should create *Unlock* notification, success, segment is off', async () => { + const data = { + eventName: 'givPower unlocked', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + metadata: { + contractName: 'GIVpower', + amount: '10', + round: 13, + transactionHash: generateRandomTxHash(), + network: 100, + }, + }; + + const result = await axios.post(sendNotificationUrl, data, { + headers: { + authorization: getGivEconomyBasicAuth(), + }, + }); + assert.equal(result.status, 200); + assert.isOk(result.data); + assert.isTrue(result.data.success); + }); + it('should create *Unlock* notification, failed invalid metadata, segment is off', async () => { + try { + const data = { + eventName: 'givPower unlocked', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + metadata: { + round: 11, + contractName: 'GIVpower', + transactionHash: generateRandomTxHash(), + network: 100, + }, + }; + + await axios.post(sendNotificationUrl, data, { + headers: { + authorization: getGivEconomyBasicAuth(), + }, + }); + // If request doesn't fail, it means this test failed + assert.isTrue(false); + } catch (e: any) { + assert.equal( + e.response.data.message, + errorMessagesEnum.IMPACT_GRAPH_VALIDATION_ERROR.message, + ); + assert.equal(e.response.data.description, '"amount" is required'); + } + }); + it('should create *UnStake* notification, success, segment is off', async () => { const data = { eventName: 'UnStake', @@ -1327,7 +1431,7 @@ function sendNotificationTestCases() { sendSegment: false, userWalletAddress: generateRandomEthereumAddress(), metadata: { - poolName: 'GIV farm', + contractName: 'GIV farm', amount: '10', network: 100, transactionHash: generateRandomTxHash(), @@ -1351,7 +1455,7 @@ function sendNotificationTestCases() { sendSegment: false, userWalletAddress: generateRandomEthereumAddress(), metadata: { - poolName: 'GIV farm', + contractName: 'GIV farm', network: 100, transactionHash: generateRandomTxHash(), }, @@ -1375,18 +1479,21 @@ function sendNotificationTestCases() { it('should create *GIVbacks are ready to claim* notification, success, segment is off', async () => { const data = { - eventName: 'GIVbacks are ready to claim', + eventName: 'GIVback is ready to claim', sendEmail: false, sendSegment: false, userWalletAddress: generateRandomEthereumAddress(), metadata: { - round: 5, + contractName: 'Gnosis Chain Token Distro', + amount: '10', + network: 100, + transactionHash: generateRandomTxHash(), }, }; const result = await axios.post(sendNotificationUrl, data, { headers: { - authorization: getGivethIoBasicAuth(), + authorization: getGivEconomyBasicAuth(), }, }); assert.equal(result.status, 200); @@ -1396,16 +1503,20 @@ function sendNotificationTestCases() { it('should create *GIVbacks are ready to claim* notification, failed invalid metadata, segment is off', async () => { try { const data = { - eventName: 'GIVbacks are ready to claim', + eventName: 'GIVback is ready to claim', sendEmail: false, sendSegment: false, userWalletAddress: generateRandomEthereumAddress(), - metadata: {}, + metadata: { + contractName: 'Gnosis Chain Token Distro', + network: 100, + transactionHash: generateRandomTxHash(), + }, }; await axios.post(sendNotificationUrl, data, { headers: { - authorization: getGivethIoBasicAuth(), + authorization: getGivEconomyBasicAuth(), }, }); // If request doesn't fail, it means this test failed @@ -1415,7 +1526,7 @@ function sendNotificationTestCases() { e.response.data.message, errorMessagesEnum.IMPACT_GRAPH_VALIDATION_ERROR.message, ); - assert.equal(e.response.data.description, '"round" is required'); + assert.equal(e.response.data.description, '"amount" is required'); } }); @@ -2240,3 +2351,223 @@ function sendNotificationTestCases() { assert.equal(createdNotification?.createdAt.getTime(), creationTime); }); } + +function sendBulkNotificationsTestCases() { + it('should create two *project liked* notifications, success, segment is off', async () => { + const data = { + notifications: [ + { + eventName: 'project liked', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + trackId: `${new Date().getTime()}-${generateRandomString(30)}`, + metadata: { + projectTitle, + projectLink, + }, + }, + { + eventName: 'project liked', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + trackId: `${new Date().getTime()}-${generateRandomString(30)}`, + metadata: { + projectTitle, + projectLink, + }, + }, + ], + }; + const result = await axios.post(sendBulkNotificationsUrl, data, { + headers: { + authorization: getGivethIoBasicAuth(), + }, + }); + assert.equal(result.status, 200); + assert.isOk(result.data); + assert.isTrue(result.data.success); + assert.isOk( + await findNotificationByTrackId(data.notifications[0].trackId as string), + ); + assert.isOk( + await findNotificationByTrackId(data.notifications[1].trackId as string), + ); + }); + it('should create two *project liked* notifications, success 100 notification', async () => { + const notifications = []; + for (let i = 0; i < 100; i++) { + notifications.push({ + eventName: 'project liked', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + trackId: `${new Date().getTime()}-${generateRandomString(30)}`, + metadata: { + projectTitle, + projectLink, + }, + }); + } + const data = { + notifications, + }; + const result = await axios.post(sendBulkNotificationsUrl, data, { + headers: { + authorization: getGivethIoBasicAuth(), + }, + }); + assert.equal(result.status, 200); + assert.isOk(result.data); + assert.isTrue(result.data.success); + for (const notification of data.notifications) { + assert.isOk( + await findNotificationByTrackId(notification.trackId as string), + ); + } + }); + it('should create two *project liked* notifications, failed for more than 100', async () => { + const notifications = []; + for (let i = 0; i < 101; i++) { + notifications.push({ + eventName: 'project liked', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + trackId: `${new Date().getTime()}-${generateRandomString(30)}`, + metadata: { + projectTitle, + projectLink, + }, + }); + } + const data = { + notifications, + }; + try { + await axios.post(sendBulkNotificationsUrl, data, { + headers: { + authorization: getGivethIoBasicAuth(), + }, + }); + assert.isTrue(false); + } catch (e: any) { + assert.equal( + e.response.data.message, + errorMessagesEnum.IMPACT_GRAPH_VALIDATION_ERROR.message, + ); + assert.equal( + e.response.data.description, + '"notifications" must contain less than or equal to 100 items', + ); + } + }); + it('should create two *project liked* notifications, failed because two notifications have same trackIds', async () => { + const trackId = `${new Date().getTime()}-${generateRandomString(30)}`; + const notifications = [ + { + eventName: 'project liked', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + trackId, + metadata: { + projectTitle, + projectLink, + }, + }, + { + eventName: 'project liked', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + trackId, + metadata: { + projectTitle, + projectLink, + }, + }, + ]; + try { + await axios.post( + sendBulkNotificationsUrl, + { notifications }, + { + headers: { + authorization: getGivethIoBasicAuth(), + }, + }, + ); + assert.isTrue(false); + } catch (e: any) { + assert.equal( + e.response.data.message, + errorMessages.THERE_IS_SOME_ITEMS_WITH_SAME_TRACK_ID, + ); + } + }); + it('should create two *project liked* notifications, failed for empty array', async () => { + const data = { + notifications: [], + }; + try { + await axios.post(sendBulkNotificationsUrl, data, { + headers: { + authorization: getGivethIoBasicAuth(), + }, + }); + assert.isTrue(false); + } catch (e: any) { + assert.equal( + e.response.data.message, + errorMessagesEnum.IMPACT_GRAPH_VALIDATION_ERROR.message, + ); + assert.equal( + e.response.data.description, + '"notifications" must contain at least 1 items', + ); + } + }); + it('should create two *project liked* notifications, failed if one notification had problem', async () => { + const data = { + notifications: [ + { + eventName: 'project liked', + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + trackId: `${new Date().getTime()}-${generateRandomString(30)}`, + metadata: { + projectTitle, + projectLink, + }, + }, + { + eventName: 'project liked2', + trackId: `${new Date().getTime()}-${generateRandomString(30)}`, + sendEmail: false, + sendSegment: false, + userWalletAddress: generateRandomEthereumAddress(), + metadata: { + projectTitle, + projectLink, + }, + }, + ], + }; + try { + await axios.post(sendBulkNotificationsUrl, data, { + headers: { + authorization: getGivethIoBasicAuth(), + }, + }); + assert.isTrue(false); + } catch (e: any) { + assert.equal( + e.response.data.message, + errorMessages.INVALID_NOTIFICATION_TYPE, + ); + } + }); +} diff --git a/src/routes/v1/notificationRouter.ts b/src/routes/v1/notificationRouter.ts index 6b2cd69..d34263e 100644 --- a/src/routes/v1/notificationRouter.ts +++ b/src/routes/v1/notificationRouter.ts @@ -27,6 +27,25 @@ notificationRouter.post( }, ); +notificationRouter.post( + '/thirdParty/notificationsBulk', + authenticateThirdPartyServiceToken, + async (req: Request, res: Response, next) => { + const { microService } = res.locals; + try { + const result = await notificationsController.sendBulkNotification( + req.body, + { + microService, + }, + ); + return sendStandardResponse({ res, result }); + } catch (e) { + next(e); + } + }, +); + notificationRouter.get( '/notifications', validateAuthMicroserviceJwt, diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts new file mode 100644 index 0000000..87e6c2b --- /dev/null +++ b/src/services/notificationService.ts @@ -0,0 +1,115 @@ +import { + createNotification, + findNotificationByTrackId, +} from '../repositories/notificationRepository'; +import { errorMessages } from '../utils/errorMessages'; +import { createNewUserAddressIfNotExists } from '../repositories/userAddressRepository'; +import { getNotificationTypeByEventNameAndMicroservice } from '../repositories/notificationTypeRepository'; +import { findNotificationSettingByNotificationTypeAndUserAddress } from '../repositories/notificationSettingRepository'; +import { logger } from '../utils/logger'; +import { EMAIL_STATUSES, Notification } from '../entities/notification'; +import { SEGMENT_METADATA_SCHEMA_VALIDATOR } from '../utils/validators/segmentAndMetadataValidators'; +import { validateWithJoiSchema } from '../validators/schemaValidators'; +import { SegmentAnalyticsSingleton } from './segment/segmentAnalyticsSingleton'; +import { SendNotificationRequest } from '../types/requestResponses'; +import { StandardError } from '../types/StandardError'; + +export const sendNotification = async ( + body: SendNotificationRequest, + microService: string, +): Promise<{ + success: boolean; + message?: string; +}> => { + const { userWalletAddress, projectId } = body; + if (body.trackId && (await findNotificationByTrackId(body.trackId))) { + // We dont throw error in this case but dont create new notification neither + return { + success: true, + message: errorMessages.DUPLICATED_TRACK_ID, + }; + } + const userAddress = await createNewUserAddressIfNotExists( + userWalletAddress as string, + ); + const notificationType = await getNotificationTypeByEventNameAndMicroservice({ + eventName: body.eventName, + microService, + }); + + if (!notificationType) { + throw new StandardError({ + message: errorMessages.INVALID_NOTIFICATION_TYPE, + httpStatusCode: 400, + }); + } + const notificationSetting = + await findNotificationSettingByNotificationTypeAndUserAddress({ + notificationTypeId: notificationType.id, + userAddressId: userAddress.id, + }); + + logger.debug('notificationController.sendNotification()', { + notificationSetting, + notificationType, + }); + + const shouldSendEmail = + body.sendEmail && notificationSetting?.allowEmailNotification; + let emailStatus = shouldSendEmail + ? EMAIL_STATUSES.WAITING_TO_BE_SEND + : EMAIL_STATUSES.NO_NEED_TO_SEND; + + const segmentValidator = + SEGMENT_METADATA_SCHEMA_VALIDATOR[ + notificationType?.schemaValidator as string + ]?.segment; + + if (shouldSendEmail && body.sendSegment && segmentValidator) { + //TODO Currently sending email and segment event are tightly coupled, we can't send segment event without sending email + // And it's not good, we should find another solution to separate sending segment and email + const segmentData = body.segment?.payload; + validateWithJoiSchema(segmentData, segmentValidator); + await SegmentAnalyticsSingleton.getInstance().track({ + eventName: notificationType.emailNotificationId as string, + anonymousId: body?.segment?.anonymousId, + properties: segmentData, + analyticsUserId: body?.segment?.analyticsUserId, + }); + emailStatus = EMAIL_STATUSES.SENT; + } + + const metadataValidator = + SEGMENT_METADATA_SCHEMA_VALIDATOR[ + notificationType?.schemaValidator as string + ]?.metadata; + + if (metadataValidator) { + validateWithJoiSchema(body.metadata, metadataValidator); + } + + if (!notificationSetting?.allowDappPushNotification) { + //TODO In future we can add a create notification but with disabledNotification:true + // So we can exclude them in list of notifications + return { + success: true, + message: errorMessages.USER_TURNED_OF_THIS_NOTIFICATION_TYPE, + }; + } + const notificationData: Partial = { + notificationType, + userAddress, + email: body.email, + emailStatus, + trackId: body?.trackId, + metadata: body?.metadata, + segmentData: body.segment, + projectId, + }; + if (body.creationTime) { + // creationTime is optional and it's timestamp in milliseconds format + notificationData.createdAt = new Date(body.creationTime); + } + await createNotification(notificationData); + return { success: true }; +}; diff --git a/src/services/segment/segmentAnalyticsSingleton.ts b/src/services/segment/segmentAnalyticsSingleton.ts index 00cdcaf..71f34f5 100644 --- a/src/services/segment/segmentAnalyticsSingleton.ts +++ b/src/services/segment/segmentAnalyticsSingleton.ts @@ -23,7 +23,8 @@ export enum SegmentEvents { PROJECT_BADGE_UP_FOR_REVOKING = 'Project badge up for revoking', PROJECT_VERIFIED = 'Project verified', PROJECT_REJECTED = 'Project rejected', - GIVBACK_ARE_READY_TO_CLAIM = 'We dont have event yet', + GIVBACKS_ARE_READY_TO_CLAIM = 'We dont have event yet', + GIVBACK_IS_READY_TO_CLAIM = 'We dont have event yet', PROJECT_UNVERIFIED = 'Project unverified', PROJECT_UNVERIFIED_USERS_WHO_BOOSTED = 'Project unverified - Users Who Boosted', PROJECT_ACTIVATED = 'Project activated', diff --git a/src/types/requestResponses.ts b/src/types/requestResponses.ts index bd9a879..c63a123 100644 --- a/src/types/requestResponses.ts +++ b/src/types/requestResponses.ts @@ -4,15 +4,17 @@ interface BaseNotificationResponse { trackId?: string; } -export type SendNotificationTypeRequest = { - /** - * @example "Made donation" - */ - eventName: string; +export interface SendNotificationRequest { /** * @example "Should be unique" */ trackId?: string; + + /** + * @example "Made donation" + */ + eventName: string; + /** * @example false */ @@ -57,51 +59,24 @@ export type SendNotificationTypeRequest = { anonymousId?: string; }; metadata?: NotificationMetadata; -}; - -export type SendNotificationRequest = { - /** - * @example "1" - */ - userId: string; - - /** - * @example "1" - */ - projectId: string; - - metadata: { - /** - * @example "https://giveth.io/project/test-project - */ - projectLink?: string; - - /** - * @example "Test project" - */ - projectTitle?: string; - }; - - /** - * @example "y@giveth.io" - */ - email: string; +} +interface SendNotificationTypeRequestBulkItem extends SendNotificationRequest { /** - * @example "projectListed" + * @example "Should be unique" */ - notificationTemplate: number; + trackId: string; +} - /** - * @example true - */ - sendEmail: boolean; -}; +export interface sendBulkNotificationRequest { + notifications: SendNotificationTypeRequestBulkItem[]; +} export interface SendNotificationResponse extends BaseNotificationResponse { success: boolean; message?: string; } + export interface GetNotificationsResponse extends BaseNotificationResponse { notifications: Notification[]; count: number; diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index fa35f07..b1ad8a7 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -92,7 +92,8 @@ export const errorMessages = { JUST_ACTIVE_PROJECTS_ACCEPT_DONATION: 'Just active projects accept donation', CATEGORIES_LENGTH_SHOULD_NOT_BE_MORE_THAN_FIVE: 'Please select no more than 5 categories', - INVALID_NOTIFICATION_TYPE: 'Notification type invalid', + INVALID_NOTIFICATION_TYPE: 'Notification type is invalid', + THERE_IS_SOME_ITEMS_WITH_SAME_TRACK_ID: 'There is some ids with same trackId', DUPLICATED_TRACK_ID: 'Duplicated trackId', CATEGORIES_MUST_BE_FROM_THE_FRONTEND_SUBSELECTION: 'This category is not valid', diff --git a/src/utils/validators/segmentAndMetadataValidators.ts b/src/utils/validators/segmentAndMetadataValidators.ts index f82e325..496bfd2 100644 --- a/src/utils/validators/segmentAndMetadataValidators.ts +++ b/src/utils/validators/segmentAndMetadataValidators.ts @@ -43,7 +43,18 @@ const projectBoostedSchema = Joi.object({ }); const givPowerLockedSchema = Joi.object({ - amount: Joi.number()?.greater(0).required(), + contractName: Joi.string().required(), + amount: Joi.string().required(), + round: Joi.number()?.greater(0).required(), + transactionHash: Joi.string().required(), + network: Joi.number().required(), +}); +const givPowerUnLockedSchema = Joi.object({ + contractName: Joi.string().required(), + amount: Joi.string().required(), + round: Joi.number()?.greater(0).required(), + transactionHash: Joi.string().required(), + network: Joi.number().required(), }); const donationTrackerSchema = Joi.object({ @@ -89,7 +100,7 @@ const getDonationPriceFailedMetadataSchema = Joi.object({ txLink: Joi.string().required(), }); const stakeUnStakeSchema = Joi.object({ - poolName: Joi.string().required(), + contractName: Joi.string().required(), transactionHash: Joi.string().required(), network: Joi.number().required(), amount: Joi.string().required(), @@ -101,8 +112,18 @@ const claimSchema = Joi.object({ const adminMessageSchema = Joi.object({ linkTitle: Joi.string().required(), content: Joi.string().required(), - instruction: Joi.string().required(), - href: Joi.string().required(), + instruction: Joi.string(), + href: Joi.string(), +}); + +const rawHtmlBroadcastSchema = Joi.object({ + html: Joi.string().required(), +}); +const givBackReadyClaimSchema = Joi.object({ + contractName: Joi.string().required(), + transactionHash: Joi.string().required(), + network: Joi.number().required(), + amount: Joi.string().required(), }); export const SEGMENT_METADATA_SCHEMA_VALIDATOR: { @@ -175,6 +196,10 @@ export const SEGMENT_METADATA_SCHEMA_VALIDATOR: { metadata: projectTitleProjectLinkSchema, segment: projectRelatedTrackerSchema, }, + projectBoosted: { + metadata: projectTitleProjectLinkSchema, + segment: null, + }, sendEmailConfirmation: { metadata: null, segment: projectRelatedTrackerSchema, @@ -233,6 +258,7 @@ export const SEGMENT_METADATA_SCHEMA_VALIDATOR: { givFarmUnStake: { metadata: stakeUnStakeSchema, segment: null }, givFarmReadyToClaim: { metadata: claimSchema, segment: null }, adminMessage: { metadata: adminMessageSchema, segment: null }, + rawHtmlBroadcast: { metadata: rawHtmlBroadcastSchema, segment: null }, givPowerUserBoosted: { metadata: boostedSchema, segment: null }, givPowerUserChangedBoostedAllocation: { metadata: null, segment: null }, givPowerProjectHasBeenBoosted: { @@ -241,13 +267,17 @@ export const SEGMENT_METADATA_SCHEMA_VALIDATOR: { }, givPowerUserLockedGivPower: { metadata: givPowerLockedSchema, segment: null }, givPowerUserUnlockedGivPower: { - metadata: givPowerLockedSchema, + metadata: givPowerUnLockedSchema, segment: null, }, givPowerUserGivPowerRelockedAutoMatically: { metadata: givPowerLockedSchema, segment: null, }, + givBackReadyToClaim: { + metadata: givBackReadyClaimSchema, + segment: null, + }, }; function throwHttpErrorIfJoiValidatorFails( diff --git a/src/validators/schemaValidators.ts b/src/validators/schemaValidators.ts index 2f58f69..55fa091 100644 --- a/src/validators/schemaValidators.ts +++ b/src/validators/schemaValidators.ts @@ -24,7 +24,9 @@ const throwHttpErrorIfJoiValidatorFails = ( export const countUnreadValidator = Joi.object({ walletAddress: Joi.string().pattern(ethereumWalletAddressRegex).required(), }); + export const sendNotificationValidator = Joi.object({ + trackId: Joi.string(), projectId: Joi.string(), analyticsUserId: Joi.string(), anonymousId: Joi.string(), @@ -33,7 +35,6 @@ export const sendNotificationValidator = Joi.object({ sendEmail: Joi.boolean(), sendSegment: Joi.boolean(), email: Joi.string(), - trackId: Joi.string(), creationTime: Joi.number(), userWalletAddress: Joi.string() .pattern(ethereumWalletAddressRegex) @@ -75,6 +76,20 @@ export const sendNotificationValidator = Joi.object({ }), }); +export const sendBulkNotificationValidator = Joi.object({ + notifications: Joi.array() + .required() + .min(1) + .max(100) + .items( + sendNotificationValidator.concat( + Joi.object({ + trackId: Joi.string().required(), + }), + ), + ), +}); + export const updateNotificationSettings = Joi.object({ settings: Joi.array() .required() @@ -86,6 +101,7 @@ export const updateNotificationSettings = Joi.object({ }), ), }); + export const updateOneNotificationSetting = Joi.object({ id: Joi.number().required(), allowEmailNotification: Joi.boolean().required(),