diff --git a/config/example.env b/config/example.env index 63cd251..59c489c 100644 --- a/config/example.env +++ b/config/example.env @@ -13,9 +13,15 @@ REDIS_PORT=6490 REDIS_PASSWORD= SEGMENT_API_KEY=FAKE_API_KEY +ORTTO_API_KEY=FAKE_API_KEY +ORTTO_ACTIVITY_API=https://api-us.ortto.app/v1/activities/create #JWT_AUTHORIZATION_ADAPTER=siweMicroservice JWT_AUTHORIZATION_ADAPTER=mock AUTH_MICROSERVICE_AUTHORIZATION_URL= HOSTNAME_WHITELIST=next.giveth.io,www.next.giveth.io,staging.giveth.io,www.staging.giveth.io,localhost,giveth.io,vercel.app,www.giveth.io + + +EMAIL_ADAPTER=mock +#EMAIL_ADAPTER=ortto diff --git a/config/test.env b/config/test.env index 73ecf21..a699c35 100644 --- a/config/test.env +++ b/config/test.env @@ -11,7 +11,6 @@ REDIS_HOST=127.0.0.1 REDIS_PORT=6490 REDIS_PASSWORD= - SEGMENT_API_KEY=FAKE_API_KEY JWT_AUTHORIZATION_ADAPTER=mock @@ -26,3 +25,5 @@ GIV_ECONOMY_THIRD_PARTY_MICRO_SERVICE=giveconomy-notification-service # OPTIONAL - force logging to stdout when the value is true LOG_STDOUT=false + +EMAIL_ADAPTER=mock diff --git a/src/adapters/adapterFactory.ts b/src/adapters/adapterFactory.ts index 334e6f5..b13919a 100644 --- a/src/adapters/adapterFactory.ts +++ b/src/adapters/adapterFactory.ts @@ -1,6 +1,8 @@ import { SiweAuthenticationMicroserviceAdapter } from './jwtAuthentication/siweAuthenticationMicroserviceAdapter'; import { MockJwtAdapter } from './jwtAuthentication/mockJwtAdapter'; import { errorMessages } from '../utils/errorMessages'; +import {OrttoAdapter} from "./emailAdapter/orttoAdapter"; +import {OrttoMockAdapter} from "./emailAdapter/orttoMockAdapter"; const siweAuthenricationAdapter = new SiweAuthenticationMicroserviceAdapter(); const jwtMockAdapter = new MockJwtAdapter(); @@ -15,3 +17,17 @@ export const getJwtAuthenticationAdapter = () => { throw new Error(errorMessages.SPECIFY_JWT_AUTHENTICATION_ADAPTER); } }; + + +const orttoEmailAdapter = new OrttoAdapter() +const emailMockAdapter = new OrttoMockAdapter() +export const getEmailAdapter = () => { + switch (process.env.EMAIL_ADAPTER) { + case 'ortto': + return orttoEmailAdapter; + case 'mock': + return emailMockAdapter; + default: + throw new Error(errorMessages.SPECIFY_Email_ADAPTER); + } +}; diff --git a/src/adapters/emailAdapter/orttoAdapter.ts b/src/adapters/emailAdapter/orttoAdapter.ts new file mode 100644 index 0000000..3ce533d --- /dev/null +++ b/src/adapters/emailAdapter/orttoAdapter.ts @@ -0,0 +1,32 @@ +import { logger } from '../../utils/logger'; +import axios from 'axios'; +import {OrttoAdapterInterface} from "./orttoAdapterInterface"; + + +export class OrttoAdapter implements OrttoAdapterInterface{ + async callOrttoActivity(data: any): Promise { + try { + if (!data){ + throw new Error('callOrttoActivity input data is empty') + } + const config = { + method: 'post', + maxBodyLength: Infinity, + url: process.env.ORTTO_ACTIVITY_API, + headers: { + 'X-Api-Key': process.env.ORTTO_API_KEY as string, + 'Content-Type': 'application/json' + }, + data + }; + data.activities.map((a: any) => logger.debug('orttoActivityCall', a)); + await axios.request(config); + } catch (e) { + logger.error('orttoActivityCall error', { + error: e, + data + }); + } + } + +} diff --git a/src/adapters/emailAdapter/orttoAdapterInterface.ts b/src/adapters/emailAdapter/orttoAdapterInterface.ts new file mode 100644 index 0000000..95ea016 --- /dev/null +++ b/src/adapters/emailAdapter/orttoAdapterInterface.ts @@ -0,0 +1,5 @@ +import internal from "stream"; + +export interface OrttoAdapterInterface { + callOrttoActivity (data: any): Promise +} diff --git a/src/adapters/emailAdapter/orttoMockAdapter.ts b/src/adapters/emailAdapter/orttoMockAdapter.ts new file mode 100644 index 0000000..7c46e94 --- /dev/null +++ b/src/adapters/emailAdapter/orttoMockAdapter.ts @@ -0,0 +1,11 @@ +import { logger } from '../../utils/logger'; +import axios from 'axios'; +import {OrttoAdapterInterface} from "./orttoAdapterInterface"; + + +export class OrttoMockAdapter implements OrttoAdapterInterface{ + async callOrttoActivity(data: any): Promise { + logger.debug('OrttoMockAdapter has been called', data) + } + +} diff --git a/src/controllers/v1/notificationsController.ts b/src/controllers/v1/notificationsController.ts index cd4936a..3fb0963 100644 --- a/src/controllers/v1/notificationsController.ts +++ b/src/controllers/v1/notificationsController.ts @@ -35,7 +35,6 @@ import { markNotificationsAsRead, } from '../../repositories/notificationRepository'; import { UserAddress } from '../../entities/userAddress'; -import { Notification } from '../../entities/notification'; import { createNewUserAddressIfNotExists } from '../../repositories/userAddressRepository'; import { sendNotification } from '../../services/notificationService'; import { StandardError } from '../../types/StandardError'; diff --git a/src/routes/v1/notificationRouter.test.ts b/src/routes/v1/notificationRouter.test.ts index cc3e1bf..5d8021d 100644 --- a/src/routes/v1/notificationRouter.test.ts +++ b/src/routes/v1/notificationRouter.test.ts @@ -355,8 +355,6 @@ function sendNotificationTestCases() { sendEmail: true, sendSegment: true, segment: { - analyticsUserId: 'givethId-255', - anonymousId: 'givethId-255', payload: { title: 'Test verify and reject form emails', lastName: 'Ranjbar', @@ -1385,8 +1383,6 @@ function sendNotificationTestCases() { projectLink, }, segment: { - analyticsUserId: 'givethId-255', - anonymousId: 'givethId-255', payload: { email: 'test@giveth.com', title: 'How many photos is too many photos?', @@ -1428,8 +1424,6 @@ function sendNotificationTestCases() { projectLink, }, segment: { - analyticsUserId: 'givethId-255', - anonymousId: 'givethId-255', payload: { email: 'test@giveth.com', title: 'How many photos is too many photos?', @@ -1499,8 +1493,6 @@ function sendNotificationTestCases() { projectLink, }, segment: { - analyticsUserId: 'givethId-255', - anonymousId: 'givethId-255', payload: { email: 'test@giveth.com', title: 'How many photos is too many photos?', @@ -1542,8 +1534,6 @@ function sendNotificationTestCases() { projectLink, }, segment: { - analyticsUserId: 'givethId-255', - anonymousId: 'givethId-255', payload: { email: 'test@giveth.com', title: 'How many photos is too many photos?', diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index f76c5c7..1a66873 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -10,9 +10,171 @@ 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'; +import { NOTIFICATIONS_EVENT_NAMES, ORTTO_EVENT_NAMES } from '../types/notifications'; +import {getEmailAdapter} from "../adapters/adapterFactory"; + +const activityCreator = (payload: any, orttoEventName: NOTIFICATIONS_EVENT_NAMES) : any=> { + switch (orttoEventName) { + case NOTIFICATIONS_EVENT_NAMES.DONATION_RECEIVED: + return { + "activities": [ + { + "activity_id": `act:cm:${ORTTO_EVENT_NAMES[orttoEventName]}`, + "attributes": { + "str:cm:projecttitle": payload.title, + "int:cm:donationamount": payload.amount, + "str:cm:donationtoken": payload.token, + "str:cm:email": payload.email, + "str:cm:projectlink": payload.projectLink, + "bol:cm:verified": payload.verified, + "str:cm:transactionlink": payload.transactionLink, + }, + "fields": { + "str::email": payload.email + } + } + ] + }; + case NOTIFICATIONS_EVENT_NAMES.DRAFTED_PROJECT_ACTIVATED: + return { + "activities": [ + { + "activity_id": `act:cm:${ORTTO_EVENT_NAMES[orttoEventName]}`, + "attributes": { + "str:cm:projecttitle": payload.title, + "str:cm:email": payload.email, + "str:cm:projectlink": payload.projectLink, + "str:cm:firstname": payload.firstName, + "str:cm:lastname": payload.lastName, + }, + "fields": { + "str::email": payload.email + } + } + ] + }; + case NOTIFICATIONS_EVENT_NAMES.PROJECT_LISTED: + return { + "activities": [ + { + "activity_id": `act:cm:${ORTTO_EVENT_NAMES[orttoEventName]}`, + "attributes": { + "str:cm:projecttitle": payload.title, + "str:cm:email": payload.email, + "str:cm:projectlink": payload.projectLink, + }, + "fields": { + "str::email": payload.email + } + } + ] + }; + case NOTIFICATIONS_EVENT_NAMES.PROJECT_UNLISTED: + return { + "activities": [ + { + "activity_id": `act:cm:${ORTTO_EVENT_NAMES[orttoEventName]}`, + "attributes": { + "str:cm:projecttitle": payload.title, + "str:cm:email": payload.email, + "str:cm:projectlink": payload.projectLink, + }, + "fields": { + "str::email": payload.email + } + } + ] + }; + case NOTIFICATIONS_EVENT_NAMES.PROJECT_CANCELLED: + return { + "activities": [ + { + "activity_id": `act:cm:${ORTTO_EVENT_NAMES[orttoEventName]}`, + "attributes": { + "str:cm:projecttitle": payload.title, + "str:cm:email": payload.email, + "str:cm:projectlink": payload.projectLink, + }, + "fields": { + "str::email": payload.email + } + } + ] + }; + case NOTIFICATIONS_EVENT_NAMES.PROJECT_UPDATE_ADDED_OWNER: + return { + "activities": [ + { + "activity_id": `act:cm:${ORTTO_EVENT_NAMES[orttoEventName]}`, + "attributes": { + "str:cm:projecttitle": payload.title, + "str:cm:email": payload.email, + "str:cm:projectupdatelink": payload.projectLink + '?tab=updates', + }, + "fields": { + "str::email": payload.email + } + } + ] + }; + case NOTIFICATIONS_EVENT_NAMES.PROJECT_VERIFIED: + return { + "activities": [ + { + "activity_id": `act:cm:${ORTTO_EVENT_NAMES[orttoEventName]}`, + "attributes": { + "str:cm:projecttitle": payload.title, + "str:cm:email": payload.email, + "str:cm:projectlink": payload.projectLink, + "str:cm:verified-status": 'verified', + }, + "fields": { + "str::email": payload.email + } + } + ] + }; + case NOTIFICATIONS_EVENT_NAMES.PROJECT_UNVERIFIED: + return { + "activities": [ + { + "activity_id": `act:cm:${ORTTO_EVENT_NAMES[orttoEventName]}`, + "attributes": { + "str:cm:projecttitle": payload.title, + "str:cm:email": payload.email, + "str:cm:projectlink": payload.projectLink, + "str:cm:verified-status": 'rejected', + }, + "fields": { + "str::email": payload.email + } + } + ] + }; + case NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKED: + return { + "activities": [ + { + "activity_id": `act:cm:${ORTTO_EVENT_NAMES[orttoEventName]}`, + "attributes": { + "str:cm:projecttitle": payload.title, + "str:cm:email": payload.email, + "str:cm:projectlink": payload.projectLink, + "str:cm:verified-status": 'revoked', + }, + "fields": { + "str::email": payload.email + } + } + ] + }; + default: + logger.debug('activityCreator() invalid event name', orttoEventName) + return undefined + } +} export const sendNotification = async ( body: SendNotificationRequest, @@ -78,14 +240,10 @@ export const sendNotification = async ( 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, - }); + const emailData = body.segment?.payload; + validateWithJoiSchema(emailData, segmentValidator); + const data = activityCreator(emailData, body.eventName as NOTIFICATIONS_EVENT_NAMES); + await getEmailAdapter().callOrttoActivity(data); emailStatus = EMAIL_STATUSES.SENT; } @@ -99,7 +257,7 @@ export const sendNotification = async ( } if (!notificationSetting?.allowDappPushNotification) { - //TODO In future we can add a create notification but with disabledNotification:true + //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, diff --git a/src/types/notifications.ts b/src/types/notifications.ts new file mode 100644 index 0000000..a57883c --- /dev/null +++ b/src/types/notifications.ts @@ -0,0 +1,59 @@ +export enum NOTIFICATIONS_EVENT_NAMES { + DRAFTED_PROJECT_ACTIVATED = 'Draft published', + PROJECT_LISTED = 'Project listed', + PROJECT_UNLISTED = 'Project unlisted', + PROJECT_UNLISTED_SUPPORTED = 'Project unlisted - Users who supported', + PROJECT_LISTED_SUPPORTED = 'Project listed - Users who supported', + PROJECT_EDITED = 'Project edited', + PROJECT_BADGE_REVOKED = 'Project badge revoked', + PROJECT_BADGE_REVOKE_REMINDER = 'Project badge revoke reminder', + PROJECT_BADGE_REVOKE_WARNING = 'Project badge revoke warning', + PROJECT_BADGE_REVOKE_LAST_WARNING = 'Project badge revoke last warning', + PROJECT_BADGE_UP_FOR_REVOKING = 'Project badge up for revoking', + PROJECT_BOOSTED = 'Project boosted', + PROJECT_BOOSTED_BY_PROJECT_OWNER = 'Project boosted by project owner', + PROJECT_VERIFIED = 'Project verified', + PROJECT_VERIFIED_USERS_WHO_SUPPORT = 'Project verified - Users who supported', + + // https://github.com/Giveth/impact-graph/issues/624#issuecomment-1240364389 + PROJECT_REJECTED = 'Project unverified', + PROJECT_NOT_REVIEWED = 'Project not reviewed', + PROJECT_UNVERIFIED = 'Project unverified', + VERIFICATION_FORM_REJECTED = 'Form rejected', + PROJECT_UNVERIFIED_USERS_WHO_SUPPORT = 'Project unverified - Users who supported', + PROJECT_ACTIVATED = 'Project activated', + PROJECT_ACTIVATED_USERS_WHO_SUPPORT = 'Project activated - Users who supported', + PROJECT_DEACTIVATED = 'Project deactivated', + PROJECT_DEACTIVATED_USERS_WHO_SUPPORT = 'Project deactivated - Users who supported', + + PROJECT_CANCELLED = 'Project cancelled', + PROJECT_CANCELLED_USERS_WHO_SUPPORT = 'Project cancelled - Users who supported', + MADE_DONATION = 'Made donation', + DONATION_RECEIVED = 'Donation received', + DONATION_GET_PRICE_FAILED = 'Donation get price failed', + PROJECT_RECEIVED_HEART = 'project liked', + PROJECT_UPDATE_ADDED_OWNER = 'Project update added - owner', + PROJECT_CREATED = 'The project saved as draft', + UPDATED_PROFILE = 'Updated profile', + GET_DONATION_PRICE_FAILED = 'Get Donation Price Failed', + VERIFICATION_FORM_GOT_DRAFT_BY_ADMIN = 'Verification form got draft by admin', + RAW_HTML_BROADCAST = 'Raw HTML Broadcast', + PROJECT_ADD_AN_UPDATE_USERS_WHO_SUPPORT = 'Project update added - Users who supported', + + // https://github.com/Giveth/impact-graph/issues/774#issuecomment-1542337083 + PROJECT_HAS_RISEN_IN_THE_RANK = 'Your Project has risen in the rank', + PROJECT_HAS_A_NEW_RANK = 'Your project has a new rank', + YOUR_PROJECT_GOT_A_RANK = 'Your project got a rank', +} + +export const ORTTO_EVENT_NAMES = { + [NOTIFICATIONS_EVENT_NAMES.DONATION_RECEIVED]: 'testing-donation-received', + [NOTIFICATIONS_EVENT_NAMES.DRAFTED_PROJECT_ACTIVATED]: 'project-created', + [NOTIFICATIONS_EVENT_NAMES.PROJECT_LISTED]: 'project-listed', + [NOTIFICATIONS_EVENT_NAMES.PROJECT_UNLISTED]: 'project-unlisted', + [NOTIFICATIONS_EVENT_NAMES.PROJECT_CANCELLED]: 'project-deactivated', + [NOTIFICATIONS_EVENT_NAMES.MADE_DONATION]: 'donation-made', + [NOTIFICATIONS_EVENT_NAMES.PROJECT_UNVERIFIED]: 'project-verification', + [NOTIFICATIONS_EVENT_NAMES.PROJECT_VERIFIED]: 'project-verification', + [NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKED]: 'project-verification', +} \ No newline at end of file diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index b1ad8a7..07ed457 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -50,6 +50,7 @@ export const errorMessagesEnum = { export const errorMessages = { SPECIFY_JWT_AUTHENTICATION_ADAPTER: 'Specify authentication jwt adapter', + SPECIFY_Email_ADAPTER: 'Specify email adapter', CHANGE_API_INVALID_TITLE_OR_EIN: 'ChangeAPI title or EIN not found or invalid', INVALID_SOCIAL_NETWORK: 'Invalid social network', diff --git a/src/utils/validators/segmentAndMetadataValidators.ts b/src/utils/validators/segmentAndMetadataValidators.ts index 3a5a438..605faaa 100644 --- a/src/utils/validators/segmentAndMetadataValidators.ts +++ b/src/utils/validators/segmentAndMetadataValidators.ts @@ -27,6 +27,7 @@ const projectRelatedTrackerSchema = Joi.object({ lastName: Joi.string().allow(null, ''), OwnerId: Joi.number(), slug: Joi.string().required(), + projectLink: Joi.string().allow(null).allow(''), // it's for project updates update: Joi.string().allow(null, ''), diff --git a/src/validators/schemaValidators.ts b/src/validators/schemaValidators.ts index dad4ffd..cc88303 100644 --- a/src/validators/schemaValidators.ts +++ b/src/validators/schemaValidators.ts @@ -56,6 +56,7 @@ export const sendNotificationValidator = Joi.object({ title: Joi.string(), slug: Joi.string(), firstName: Joi.string().allow(null).allow(''), + projectLink: Joi.string().allow(null).allow(''), // Donation related attributes projectOwnerId: Joi.string(),