diff --git a/src/adapters/notifications/MockNotificationAdapter.ts b/src/adapters/notifications/MockNotificationAdapter.ts index e5b7e5935..03f7c118b 100644 --- a/src/adapters/notifications/MockNotificationAdapter.ts +++ b/src/adapters/notifications/MockNotificationAdapter.ts @@ -36,6 +36,40 @@ export class MockNotificationAdapter implements NotificationAdapterInterface { }); return Promise.resolve(undefined); } + projectBadgeRevoked(params: { project: Project }): Promise { + logger.info('MockNotificationAdapter projectBadgeRevoked', { + projectSlug: params.project.slug, + }); + return Promise.resolve(undefined); + } + + projectBadgeRevokeReminder(params: { project: Project }): Promise { + logger.info('MockNotificationAdapter projectBadgeRevokeReminder', { + projectSlug: params.project.slug, + }); + return Promise.resolve(undefined); + } + + projectBadgeRevokeWarning(params: { project: Project }): Promise { + logger.info('MockNotificationAdapter projectBadgeRevokeWarning', { + projectSlug: params.project.slug, + }); + return Promise.resolve(undefined); + } + + projectBadgeUpForRevoking(params: { project: Project }): Promise { + logger.info('MockNotificationAdapter projectBadgeUpForRevoking', { + projectSlug: params.project.slug, + }); + return Promise.resolve(undefined); + } + + projectBadgeRevokeLastWarning(params: { project: Project }): Promise { + logger.info('MockNotificationAdapter projectBadgeRevokeLastWarning', { + projectSlug: params.project.slug, + }); + return Promise.resolve(undefined); + } projectReceivedHeartReaction(params: { project: Project }): Promise { logger.info('MockNotificationAdapter projectReceivedHeartReaction', { @@ -64,6 +98,15 @@ export class MockNotificationAdapter implements NotificationAdapterInterface { }); return Promise.resolve(undefined); } + projectUpdateAdded(params: { + project: Project; + update: string; + }): Promise { + logger.info('MockNotificationAdapter projectUpdateAdded', { + projectSlug: params.project.slug, + }); + return Promise.resolve(undefined); + } projectDeListed(params: { project: Project }): Promise { logger.info('MockNotificationAdapter projectDeListed', { @@ -93,6 +136,19 @@ export class MockNotificationAdapter implements NotificationAdapterInterface { return Promise.resolve(undefined); } + projectEdited(params: { project: Project }): Promise { + logger.info('MockNotificationAdapter projectEdited', { + projectSlug: params.project.slug, + }); + return Promise.resolve(undefined); + } + projectGotDraftByAdmin(params: { project: Project }): Promise { + logger.info('MockNotificationAdapter projectGotDraftByAdmin', { + projectSlug: params.project.slug, + }); + return Promise.resolve(undefined); + } + projectReactivated(params: { project: Project }): Promise { logger.info('MockNotificationAdapter projectReactivated', { projectSlug: params.project.slug, @@ -113,4 +169,14 @@ export class MockNotificationAdapter implements NotificationAdapterInterface { }); return Promise.resolve(undefined); } + + donationGetPriceFailed(params: { + project: Project; + donationInfo: { txLink: string; reason: string }; + }): Promise { + logger.info('MockNotificationAdapter donationGetPriceFailed', { + projectSlug: params.project.slug, + }); + return Promise.resolve(undefined); + } } diff --git a/src/adapters/notifications/NotificationAdapterInterface.ts b/src/adapters/notifications/NotificationAdapterInterface.ts index 260c46621..47be86f8d 100644 --- a/src/adapters/notifications/NotificationAdapterInterface.ts +++ b/src/adapters/notifications/NotificationAdapterInterface.ts @@ -17,6 +17,11 @@ export interface NotificationAdapterInterface { projectReceivedHeartReaction(params: { project: Project }): Promise; projectVerified(params: { project: Project }): Promise; + projectBadgeRevoked(params: { project: Project }): Promise; + projectBadgeRevokeReminder(params: { project: Project }): Promise; + projectBadgeRevokeWarning(params: { project: Project }): Promise; + projectBadgeRevokeLastWarning(params: { project: Project }): Promise; + projectBadgeUpForRevoking(params: { project: Project }): Promise; projectUnVerified(params: { project: Project }): Promise; projectListed(params: { project: Project }): Promise; @@ -25,9 +30,19 @@ export interface NotificationAdapterInterface { projectSavedAsDraft(params: { project: Project }): Promise; projectPublished(params: { project: Project }): Promise; + projectEdited(params: { project: Project }): Promise; + projectGotDraftByAdmin(params: { project: Project }): Promise; projectCancelled(params: { project: Project }): Promise; + projectUpdateAdded(params: { + project: Project; + update: string; + }): Promise; projectDeactivated(params: { project: Project }): Promise; projectReactivated(params: { project: Project }): Promise; ProfileIsCompleted(params: { user: User }): Promise; ProfileNeedToBeCompleted(params: { user: User }): Promise; + donationGetPriceFailed(params: { + project: Project; + donationInfo: { txLink: string; reason: string }; + }): Promise; } diff --git a/src/adapters/notifications/NotificationCenterAdapter.ts b/src/adapters/notifications/NotificationCenterAdapter.ts index f44dfa615..ef8a5dec3 100644 --- a/src/adapters/notifications/NotificationCenterAdapter.ts +++ b/src/adapters/notifications/NotificationCenterAdapter.ts @@ -16,11 +16,23 @@ const notificationCenterUsername = process.env.NOTIFICATION_CENTER_USERNAME; const notificationCenterPassword = process.env.NOTIFICATION_CENTER_PASSWORD; const notificationCenterBaseUrl = process.env.NOTIFICATION_CENTER_BASE_URL; +interface SegmentData { + payload: any; + analyticsUserId?: string; + anonymousId?: string; +} + const numberOfSendNotificationsConcurrentJob = Number( config.get('NUMBER_OF_FILLING_POWER_SNAPSHOT_BALANCE_CONCURRENT_JOB'), ) || 30; +interface SegmentData { + payload: any; + analyticsUserId?: string; + anonymousId?: string; +} + interface ProjectRelatedNotificationsQueue { project: Project; eventName: NOTIFICATIONS_EVENT_NAMES; @@ -29,9 +41,10 @@ interface ProjectRelatedNotificationsQueue { walletAddress: string; email?: string; }; + segment?: SegmentData; } -const sendProjectRelatedNotificationsBalanceQueue = +const sendProjectRelatedNotificationsQueue = new Bull( 'send-project-related-notifications', { @@ -42,11 +55,17 @@ let isProcessingQueueEventsEnabled = false; interface SendNotificationBody { sendEmail?: boolean; + sendSegment?: boolean; eventName: string; email?: string; metadata?: any; projectId: string; userWalletAddress: string; + segment?: { + payload: any; + analyticsUserId?: string; + anonymousId?: string; + }; } export class NotificationCenterAdapter implements NotificationAdapterInterface { @@ -69,7 +88,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { logger.debug('processSendingNotifications() has been called ', { numberOfSendNotificationsConcurrentJob, }); - sendProjectRelatedNotificationsBalanceQueue.process( + sendProjectRelatedNotificationsQueue.process( numberOfSendNotificationsConcurrentJob, async (job, done) => { logger.debug('processing send notification job', job.data); @@ -90,14 +109,62 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { ); } + getSegmentDonationAttributes(params: { + user: User; + project: Project; + donation: Donation; + }) { + const { user, project, donation } = params; + return { + email: user.email, + title: project.title, + firstName: user.firstName, + projectOwnerId: project.admin, + slug: project.slug, + amount: Number(donation.amount), + transactionId: donation.transactionId.toLowerCase(), + transactionNetworkId: Number(donation.transactionNetworkId), + currency: donation.currency, + createdAt: new Date(), + toWalletAddress: donation.toWalletAddress.toLowerCase(), + donationValueUsd: donation.valueUsd, + donationValueEth: donation.valueEth, + verified: Boolean(project.verified), + transakStatus: donation.transakStatus, + }; + } + + getSegmentProjectAttributes(params: { project: Project }) { + const { project } = params; + return { + email: project?.adminUser?.email, + title: project.title, + lastName: project?.adminUser?.lastName, + firstName: project?.adminUser?.firstName, + OwnerId: project?.adminUser?.id, + slug: project.slug, + }; + } + async donationReceived(params: { donation: Donation; project: Project; }): Promise { - const { project } = params; + const { project, donation } = params; + const user = project.adminUser as User; return this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.DONATION_RECEIVED, + sendEmail: true, + segment: { + analyticsUserId: user.segmentUserId(), + anonymousId: user.segmentUserId(), + payload: this.getSegmentDonationAttributes({ + donation, + project, + user, + }), + }, }); } @@ -106,7 +173,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { project: Project; donor: User; }): Promise { - const { project, donor } = params; + const { project, donor, donation } = params; return this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.MADE_DONATION, @@ -114,14 +181,145 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { email: donor.email, walletAddress: donor.walletAddress as string, }, + sendEmail: true, + segment: { + analyticsUserId: donor.segmentUserId(), + anonymousId: donor.segmentUserId(), + payload: { + ...this.getSegmentDonationAttributes({ + donation, + project, + user: donor, + }), + + // We just want this to be donation sent event not made donation, so don put it in getSegmentDonationAttributes() + // see https://github.com/Giveth/impact-graph/pull/716 + fromWalletAddress: donation.fromWalletAddress.toLowerCase(), + }, + }, }); } async projectVerified(params: { project: Project }): Promise { const { project } = params; + const user = project.adminUser as User; return this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_VERIFIED, + sendEmail: true, + segment: { + analyticsUserId: user.segmentUserId(), + anonymousId: user.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, + }); + } + + async projectBadgeRevoked(params: { project: Project }): Promise { + const { project } = params; + const user = project.adminUser as User; + return this.sendProjectRelatedNotification({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKED, + sendEmail: true, + segment: { + analyticsUserId: user.segmentUserId(), + anonymousId: user.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, + }); + } + + async projectBadgeRevokeReminder(params: { + project: Project; + }): Promise { + const { project } = params; + const user = project.adminUser as User; + return this.sendProjectRelatedNotification({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKE_REMINDER, + sendEmail: true, + segment: { + analyticsUserId: user.segmentUserId(), + anonymousId: user.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, + }); + } + + async projectBadgeRevokeWarning(params: { project: Project }): Promise { + const { project } = params; + const user = project.adminUser as User; + return this.sendProjectRelatedNotification({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKE_WARNING, + sendEmail: true, + segment: { + analyticsUserId: user.segmentUserId(), + anonymousId: user.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, + }); + } + + async projectBadgeRevokeLastWarning(params: { + project: Project; + }): Promise { + const { project } = params; + const user = project.adminUser as User; + return this.sendProjectRelatedNotification({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKE_LAST_WARNING, + sendEmail: true, + segment: { + analyticsUserId: user.segmentUserId(), + anonymousId: user.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, + }); + } + + async projectBadgeUpForRevoking(params: { project: Project }): Promise { + const { project } = params; + const user = project.adminUser as User; + return this.sendProjectRelatedNotification({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_UP_FOR_REVOKING, + sendEmail: true, + segment: { + analyticsUserId: user.segmentUserId(), + anonymousId: user.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, + }); + } + + async projectUnVerified(params: { project: Project }): Promise { + const { project } = params; + const user = project.adminUser as User; + return this.sendProjectRelatedNotification({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UNVERIFIED, + sendEmail: true, + segment: { + analyticsUserId: user.segmentUserId(), + anonymousId: user.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, }); } @@ -148,7 +346,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { const donors = await findUsersWhoDonatedToProject(project.id); donors.map(user => - sendProjectRelatedNotificationsBalanceQueue.add({ + sendProjectRelatedNotificationsQueue.add({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_CANCELLED_DONORS, user, @@ -157,16 +355,58 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { const usersWhoLiked = await findUsersWhoLikedProject(project.id); usersWhoLiked.map(user => - sendProjectRelatedNotificationsBalanceQueue.add({ + sendProjectRelatedNotificationsQueue.add({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_CANCELLED_USERS_WHO_LIKED, user, }), ); + const projectOwner = project?.adminUser as User; await this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_CANCELLED, + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, + }); + } + + async projectUpdateAdded(params: { + project: Project; + update: string; + }): Promise { + const { project, update } = params; + + const donors = await findUsersWhoDonatedToProject(project.id); + donors.map(user => + sendProjectRelatedNotificationsQueue.add({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UPDATED_DONOR, + user, + }), + ); + + const projectOwner = project?.adminUser as User; + await this.sendProjectRelatedNotification({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UPDATED_OWNER, + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: { + ...this.getSegmentProjectAttributes({ + project, + }), + update, + }, + }, }); } @@ -175,7 +415,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { const donors = await findUsersWhoDonatedToProject(project.id); donors.map(user => - sendProjectRelatedNotificationsBalanceQueue.add({ + sendProjectRelatedNotificationsQueue.add({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UNLISTED_DONORS, user, @@ -184,15 +424,26 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { const usersWhoLiked = await findUsersWhoLikedProject(project.id); usersWhoLiked.map(user => - sendProjectRelatedNotificationsBalanceQueue.add({ + sendProjectRelatedNotificationsQueue.add({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UNLISTED_USERS_WHO_LIKED, user, }), ); + + const projectOwner = project?.adminUser as User; await this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UNLISTED, + + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, }); } @@ -206,7 +457,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { }; const donors = await findUsersWhoDonatedToProject(project.id); donors.map(user => - sendProjectRelatedNotificationsBalanceQueue.add({ + sendProjectRelatedNotificationsQueue.add({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_DEACTIVATED_DONORS, user, @@ -216,7 +467,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { const usersWhoLiked = await findUsersWhoLikedProject(project.id); usersWhoLiked.map(user => - sendProjectRelatedNotificationsBalanceQueue.add({ + sendProjectRelatedNotificationsQueue.add({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_DEACTIVATED_USERS_WHO_LIKED, @@ -224,51 +475,157 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { metadata, }), ); + + const projectOwner = project?.adminUser as User; await this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_DEACTIVATED, metadata, + + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, }); } async projectListed(params: { project: Project }): Promise { const { project } = params; + const projectOwner = project?.adminUser as User; await this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_LISTED, + + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, + }); + } + + async projectEdited(params: { project: Project }): Promise { + const { project } = params; + const projectOwner = project?.adminUser as User; + + await this.sendProjectRelatedNotification({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_EDITED, + + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, + }); + } + async projectGotDraftByAdmin(params: { project: Project }): Promise { + const { project } = params; + const projectOwner = project?.adminUser as User; + + await this.sendProjectRelatedNotification({ + project, + eventName: NOTIFICATIONS_EVENT_NAMES.VERIFICATION_FORM_GOT_DRAFT_BY_ADMIN, + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, }); } projectPublished(params: { project: Project }): Promise { const { project } = params; + const projectOwner = project?.adminUser as User; return this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.DRAFTED_PROJECT_ACTIVATED, + + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, }); } projectReactivated(params: { project: Project }): Promise { const { project } = params; + const projectOwner = project?.adminUser as User; return this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_ACTIVATED, + + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, }); } projectSavedAsDraft(params: { project: Project }): Promise { const { project } = params; + const projectOwner = project?.adminUser as User; + return this.sendProjectRelatedNotification({ project, eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_CREATED, + + sendEmail: true, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, }); } - projectUnVerified(params: { project: Project }): Promise { - const { project } = params; + donationGetPriceFailed(params: { + project: Project; + donationInfo: { txLink: string; reason: string }; + }): Promise { + const { project, donationInfo } = params; + const { txLink, reason } = donationInfo; + const projectOwner = project?.adminUser as User; + return this.sendProjectRelatedNotification({ project, - eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UNVERIFIED, + eventName: NOTIFICATIONS_EVENT_NAMES.DONATION_GET_PRICE_FAILED, + metadata: { + txLink, + reason, + }, + sendEmail: false, + segment: { + analyticsUserId: projectOwner.segmentUserId(), + anonymousId: projectOwner.segmentUserId(), + payload: this.getSegmentProjectAttributes({ + project, + }), + }, }); } @@ -280,14 +637,15 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { walletAddress: string; email?: string; }; + segment?: SegmentData; + sendEmail?: boolean; }): Promise { - const { project, eventName, metadata, user } = params; + const { project, eventName, metadata, user, segment, sendEmail } = params; const receivedUser = user || (project.adminUser as User); return this.callSendNotification({ eventName, email: receivedUser.email, - // currently Segment handle sending emails, so notification-center doesnt need to send that - sendEmail: false, + sendEmail: sendEmail || false, userWalletAddress: receivedUser.walletAddress as string, projectId: String(project.id), metadata: { @@ -295,6 +653,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { projectLink: `${process.env.WEBSITE_URL}/project/${project.slug}`, ...metadata, }, + segment, }); } diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index c9ee48ac1..f302f71cc 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -24,9 +24,9 @@ export enum NOTIFICATIONS_EVENT_NAMES { PROJECT_CANCELLED = 'Project cancelled', PROJECT_CANCELLED_DONORS = 'Project cancelled - Donors', PROJECT_CANCELLED_USERS_WHO_LIKED = 'Project cancelled - Users Who Liked', - SEND_EMAIL_CONFIRMATION = 'Send email confirmation', MADE_DONATION = 'Made donation', DONATION_RECEIVED = 'Donation received', + DONATION_GET_PRICE_FAILED = 'Donation get price failed', PROJECT_RECEIVED_HEART = 'project liked', PROJECT_UPDATED_DONOR = 'Project updated - donor', PROJECT_UPDATED_OWNER = 'Project updated - owner', diff --git a/src/entities/project.ts b/src/entities/project.ts index 4ab440197..5b7d6dba6 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -24,7 +24,6 @@ import { Reaction } from './reaction'; import { Category } from './category'; import { User } from './user'; import { ProjectStatus } from './projectStatus'; -import ProjectTracker from '../services/segment/projectTracker'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; import { Int } from 'type-graphql/dist/scalars/aliases'; import { ProjectStatusHistory } from './projectStatusHistory'; @@ -337,19 +336,6 @@ class Project extends BaseEntity { * Custom Query Builders to chain together */ - static notifySegment(project: Project, eventName: NOTIFICATIONS_EVENT_NAMES) { - new ProjectTracker(project, eventName).track(); - } - - static sendBulkEventsToSegment( - projects: [Project], - eventName: NOTIFICATIONS_EVENT_NAMES, - ) { - for (const project of projects) { - this.notifySegment(project, eventName); - } - } - // only projects with status active can be listed automatically static pendingReviewSince(maximumDaysForListing: Number) { const maxDaysForListing = moment() diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index bf6b5b662..fb4a47219 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -17,6 +17,7 @@ export const findProjectById = ( return Project.createQueryBuilder('project') .leftJoinAndSelect('project.status', 'status') + .leftJoinAndSelect('project.organization', 'organization') .leftJoinAndSelect('project.addresses', 'addresses') .leftJoin('project.adminUser', 'user') .addSelect(publicSelectionFields) @@ -32,6 +33,7 @@ export const projectsWithoutUpdateAfterTimeFrame = async (date: Date) => { 'project.projectVerificationForm', 'projectVerificationForm', ) + .leftJoin('project.adminUser', 'user') .where('project.isImported = false') .andWhere('project.verified = true') .andWhere('project.updatedAt < :badgeRevokingDate', { @@ -99,7 +101,7 @@ export const verifyProject = async (params: { verified: boolean; projectId: number; }): Promise => { - const project = await Project.findOne({ id: params.projectId }); + const project = await findProjectById(params.projectId); if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 202cbfd9f..62c982bd2 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -35,7 +35,6 @@ import { updateUserTotalDonated, updateUserTotalReceived, } from '../services/userService'; -import { getCampaignDonations } from '../services/trace/traceService'; import { createDonationQueryValidator, getDonationsQueryValidator, @@ -54,6 +53,8 @@ import { sleep } from '../utils/utils'; import { findProjectRecipientAddressByNetworkId } from '../repositories/projectAddressRepository'; import { MainCategory } from '../entities/mainCategory'; import { SegmentAnalyticsSingleton } from '../services/segment/segmentAnalyticsSingleton'; +import { getNotificationAdapter } from '../adapters/adaptersFactory'; +import { findProjectById } from '../repositories/projectRepository'; @ObjectType() class PaginateDonations { @@ -357,74 +358,61 @@ export class DonationResolver { throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); } - if (traceable) { - const { total, donations } = await getCampaignDonations({ - campaignId: project.traceCampaignId as string, - take, - skip, - }); - return { - donations, - totalCount: total, - totalUsdBalance: project.totalTraceDonations, - }; - } else { - const query = this.donationRepository - .createQueryBuilder('donation') - .leftJoin('donation.user', 'user') - .addSelect(publicSelectionFields) - .where(`donation.projectId = ${projectId}`) - .orderBy( - `donation.${orderBy.field}`, - orderBy.direction, - nullDirection[orderBy.direction as string], - ); - - if (status) { - query.andWhere(`donation.status = :status`, { - status, - }); - } + const query = this.donationRepository + .createQueryBuilder('donation') + .leftJoin('donation.user', 'user') + .addSelect(publicSelectionFields) + .where(`donation.projectId = ${projectId}`) + .orderBy( + `donation.${orderBy.field}`, + orderBy.direction, + nullDirection[orderBy.direction as string], + ); - if (searchTerm) { - query.andWhere( - new Brackets(qb => { - qb.where( - '(user.name ILIKE :searchTerm AND donation.anonymous = false)', - { - searchTerm: `%${searchTerm}%`, - }, - ) - .orWhere('donation.toWalletAddress ILIKE :searchTerm', { - searchTerm: `%${searchTerm}%`, - }) - .orWhere('donation.currency ILIKE :searchTerm', { - searchTerm: `%${searchTerm}%`, - }); - - // WalletAddresses are translanted to huge integers - // this breaks postgresql query integer limit - if (!Web3.utils.isAddress(searchTerm)) { - const amount = Number(searchTerm); - - qb.orWhere('donation.amount = :number', { - number: amount, - }); - } - }), - ); - } + if (status) { + query.andWhere(`donation.status = :status`, { + status, + }); + } - const [donations, donationsCount] = await query - .take(take) - .skip(skip) - .getManyAndCount(); - return { - donations, - totalCount: donationsCount, - totalUsdBalance: project.totalDonations, - }; + if (searchTerm) { + query.andWhere( + new Brackets(qb => { + qb.where( + '(user.name ILIKE :searchTerm AND donation.anonymous = false)', + { + searchTerm: `%${searchTerm}%`, + }, + ) + .orWhere('donation.toWalletAddress ILIKE :searchTerm', { + searchTerm: `%${searchTerm}%`, + }) + .orWhere('donation.currency ILIKE :searchTerm', { + searchTerm: `%${searchTerm}%`, + }); + + // WalletAddresses are translanted to huge integers + // this breaks postgresql query integer limit + if (!Web3.utils.isAddress(searchTerm)) { + const amount = Number(searchTerm); + + qb.orWhere('donation.amount = :number', { + number: amount, + }); + } + }), + ); } + + const [donations, donationsCount] = await query + .take(take) + .skip(skip) + .getManyAndCount(); + return { + donations, + totalCount: donationsCount, + totalUsdBalance: project.totalDonations, + }; } @Query(returns => [Token], { nullable: true }) @@ -537,13 +525,7 @@ export class DonationResolver { ? NETWORK_IDS.MAIN_NET : transactionNetworkId; - const project = await Project.createQueryBuilder('project') - .leftJoinAndSelect('project.organization', 'organization') - .leftJoinAndSelect('project.status', 'status') - .where(`project.id = :projectId`, { - projectId, - }) - .getOne(); + const project = await findProjectById(projectId); if (!project) throw new Error( @@ -635,12 +617,16 @@ export class DonationResolver { error: e, donation, }); - SegmentAnalyticsSingleton.getInstance().track( - NOTIFICATIONS_EVENT_NAMES.GET_DONATION_PRICE_FAILED, - userId, - donation, - null, - ); + + await getNotificationAdapter().donationGetPriceFailed({ + project, + donationInfo: { + reason: 'Getting price failed', + + // TODO Add txLink + txLink: donation.transactionId, + }, + }); SentryLogger.captureException( new Error('Error in getting price from monoswap'), { diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index ea40c382a..6fa210535 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1,4 +1,3 @@ -import NotificationPayload from '../entities/notificationPayload'; import { Reaction } from '../entities/reaction'; import { OrderField, @@ -57,7 +56,6 @@ import { validateProjectWalletAddress, } from '../utils/validators/projectValidator'; import { updateTotalProjectUpdatesOfAProject } from '../services/projectUpdatesService'; -import { dispatchProjectUpdateEvent } from '../services/trace/traceService'; import { logger } from '../utils/logger'; import { SelectQueryBuilder } from 'typeorm/query-builder/SelectQueryBuilder'; import { getLoggedInUser } from '../services/authorizationServices'; @@ -1030,10 +1028,8 @@ export class ProjectResolver { }); // Edit emails - Project.notifySegment(project, NOTIFICATIONS_EVENT_NAMES.PROJECT_EDITED); + await getNotificationAdapter().projectEdited({ project }); - // We dont wait for trace reponse, because it may increase our response time - dispatchProjectUpdateEvent(project); return project; } @@ -1216,30 +1212,6 @@ export class ProjectResolver { }); await ProjectUpdate.save(update); - const payload: NotificationPayload = { - id: 1, - message: 'A new project was created', - }; - const segmentProject = { - email: user.email, - title: project.title, - lastName: user.lastName, - firstName: user.firstName, - OwnerId: user.id, - slug: project.slug, - walletAddress: project.walletAddress, - }; - if (status?.id === ProjStatus.active) { - SegmentAnalyticsSingleton.getInstance().track( - NOTIFICATIONS_EVENT_NAMES.PROJECT_CREATED, - `givethId-${ctx.req.user.userId}`, - segmentProject, - null, - ); - } - - await pubSub.publish('NOTIFICATIONS', payload); - if (projectInput.isDraft) { await getNotificationAdapter().projectSavedAsDraft({ project: newProject, @@ -1270,7 +1242,7 @@ export class ProjectResolver { if (!owner) throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); - const project = await Project.findOne({ id: projectId }); + const project = await findProjectById(projectId); if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); @@ -1300,46 +1272,9 @@ export class ProjectResolver { await updateTotalProjectUpdatesOfAProject(update.projectId); - SegmentAnalyticsSingleton.getInstance().track( - NOTIFICATIONS_EVENT_NAMES.PROJECT_UPDATED_OWNER, - `givethId-${user.userId}`, - projectUpdateInfo, - null, - ); - - const donations = await this.donationRepository.find({ - where: { project: { id: project?.id } }, - relations: ['user'], - }); - - const projectDonors = donations?.map(donation => { - return donation.user; - }); - const uniqueDonors = projectDonors?.filter((currentDonor, index) => { - return ( - currentDonor != null && - projectDonors.findIndex( - duplicateDonor => duplicateDonor.id === currentDonor.id, - ) === index - ); - }); - - uniqueDonors?.forEach(donor => { - const donorUpdateInfo = { - title: project.title, - projectId: project.id, - projectOwnerId: project.admin, - slug: project.slug, - update: title, - email: donor.email, - firstName: donor.firstName, - }; - SegmentAnalyticsSingleton.getInstance().track( - NOTIFICATIONS_EVENT_NAMES.PROJECT_UPDATED_DONOR, - `givethId-${donor.id}`, - donorUpdateInfo, - null, - ); + await getNotificationAdapter().projectUpdateAdded({ + project, + update: title, }); return save; } @@ -1778,21 +1713,6 @@ export class ProjectResolver { reasonId, }); - const segmentProject = { - email: user.email, - title: project.title, - LastName: user.lastName, - FirstName: user.firstName, - OwnerId: project.admin, - slug: project.slug, - }; - - SegmentAnalyticsSingleton.getInstance().track( - NOTIFICATIONS_EVENT_NAMES.PROJECT_DEACTIVATED, - `givethId-${ctx.req.user.userId}`, - segmentProject, - null, - ); await getNotificationAdapter().projectDeactivated({ project, }); @@ -1801,7 +1721,6 @@ export class ProjectResolver { logger.error('projectResolver.deactivateProject() error', error); SentryLogger.captureException(error); throw error; - return false; } } @Mutation(returns => Boolean) @@ -1816,27 +1735,9 @@ export class ProjectResolver { statusId: ProjStatus.active, user, }); - const segmentEventToDispatch = - project.prevStatusId === ProjStatus.drafted - ? NOTIFICATIONS_EVENT_NAMES.DRAFTED_PROJECT_ACTIVATED - : NOTIFICATIONS_EVENT_NAMES.PROJECT_ACTIVATED; project.listed = null; await project.save(); - const segmentProject = { - email: user.email, - title: project.title, - LastName: user.lastName, - FirstName: user.firstName, - OwnerId: project.admin, - slug: project.slug, - }; - SegmentAnalyticsSingleton.getInstance().track( - segmentEventToDispatch, - `givethId-${ctx.req.user.userId}`, - segmentProject, - null, - ); if (project.prevStatusId === ProjStatus.drafted) { await getNotificationAdapter().projectPublished({ diff --git a/src/resolvers/projectVerificationFormResolver.ts b/src/resolvers/projectVerificationFormResolver.ts index 0b162c471..9474ae112 100644 --- a/src/resolvers/projectVerificationFormResolver.ts +++ b/src/resolvers/projectVerificationFormResolver.ts @@ -31,7 +31,6 @@ import { countriesList } from '../utils/utils'; import { Country } from '../entities/Country'; import { sendMailConfirmationEmail } from '../services/mailerService'; import moment from 'moment'; -import { SegmentAnalyticsSingleton } from '../services/segment/segmentAnalyticsSingleton'; const dappUrl = process.env.FRONTEND_URL as string; @@ -182,21 +181,8 @@ export class ProjectVerificationFormResolver { projectVerificationForm.emailConfirmationSentAt = new Date(); await projectVerificationForm.save(); - const callbackUrl = `https://${dappUrl}/verification/${project.slug}/${token}`; - const emailConfirmationData = { - email, - callbackUrl, - }; - await sendMailConfirmationEmail(email, project, token); - SegmentAnalyticsSingleton.getInstance().track( - NOTIFICATIONS_EVENT_NAMES.SEND_EMAIL_CONFIRMATION, - `givethId-${userId}`, - emailConfirmationData, - null, - ); - return projectVerificationForm; } catch (e) { logger.error('sendEmailConfirmation() error', e); diff --git a/src/server/adminBro.ts b/src/server/adminBro.ts index 1a6b49a84..6226028b0 100644 --- a/src/server/adminBro.ts +++ b/src/server/adminBro.ts @@ -12,7 +12,6 @@ import { User, UserRole } from '../entities/user'; import AdminBroExpress from '@admin-bro/express'; import config from '../config'; import { redis } from '../redis'; -import { dispatchProjectUpdateEvent } from '../services/trace/traceService'; import { Database, Resource } from '@admin-bro/typeorm'; import { SelectQueryBuilder } from 'typeorm'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; @@ -80,7 +79,6 @@ import { verifyMultipleProjects, verifyProject, } from '../repositories/projectRepository'; -import { SocialProfile } from '../entities/socialProfile'; import { RecordJSON } from 'admin-bro/src/frontend/interfaces/record-json.interface'; import { findSocialProfilesByProjectId } from '../repositories/socialProfileRepository'; import { updateTotalDonationsOfProject } from '../services/donationService'; @@ -1379,7 +1377,6 @@ const getAdminBroInstance = async () => { if (project) { // Not required for now // Project.notifySegment(project, SegmentEvents.PROJECT_EDITED); - await dispatchProjectUpdateEvent(project); // As we dont what fields has changed (listed, verified, ..), I just added new status and a description that project has been edited await Project.addProjectStatusHistoryRecord({ @@ -1967,15 +1964,7 @@ export const listDelist = async ( .returning('*') .updateEntity(true) .execute(); - - Project.sendBulkEventsToSegment( - projects.raw, - list - ? NOTIFICATIONS_EVENT_NAMES.PROJECT_LISTED - : NOTIFICATIONS_EVENT_NAMES.PROJECT_UNLISTED, - ); for (const project of projects.raw) { - await dispatchProjectUpdateEvent(project); await Project.addProjectStatusHistoryRecord({ project, status: project.status, @@ -2060,13 +2049,11 @@ export const verifySingleVerificationForm = async ( adminId: currentAdmin.id, }); const projectId = verificationForm.projectId; - let project = (await findProjectById(projectId)) as Project; - project = await verifyProject({ verified, projectId }); + const project = await verifyProject({ verified, projectId }); if (verified) { await updateProjectWithVerificationForm(verificationForm, project); } - Project.notifySegment(project, segmentEvent); if (verified) { await getNotificationAdapter().projectVerified({ project, @@ -2128,17 +2115,13 @@ export const makeEditableByUser = async ( ), ); } - - const segmentEvent = - NOTIFICATIONS_EVENT_NAMES.VERIFICATION_FORM_GOT_DRAFT_BY_ADMIN; - const verificationForm = await makeFormDraft({ formId, adminId: currentAdmin.id, }); const projectId = verificationForm.projectId; const project = (await findProjectById(projectId)) as Project; - Project.notifySegment(project, segmentEvent); + await getNotificationAdapter().projectGotDraftByAdmin({ project }); responseMessage = `Project(s) successfully got draft`; } catch (error) { @@ -2193,8 +2176,7 @@ export const verifyVerificationForms = async ( const segmentEvent = verified ? NOTIFICATIONS_EVENT_NAMES.PROJECT_VERIFIED : NOTIFICATIONS_EVENT_NAMES.PROJECT_REJECTED; - - Project.sendBulkEventsToSegment(projects.raw, segmentEvent); + // TODO send appropriate notification const projectIds = projects.raw.map(project => { return project.id; }); @@ -2263,17 +2245,7 @@ export const verifyProjects = async ( .updateEntity(true) .execute(); - let segmentEvent = verified - ? NOTIFICATIONS_EVENT_NAMES.PROJECT_VERIFIED - : NOTIFICATIONS_EVENT_NAMES.PROJECT_UNVERIFIED; - - segmentEvent = revokeBadge - ? NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKED - : segmentEvent; - - Project.sendBulkEventsToSegment(projects.raw, segmentEvent); for (const project of projects.raw) { - await dispatchProjectUpdateEvent(project); await Project.addProjectStatusHistoryRecord({ project, status: project.status, @@ -2287,9 +2259,10 @@ export const verifyProjects = async ( if (revokeBadge) { projectWithAdmin.verificationStatus = RevokeSteps.Revoked; await projectWithAdmin.save(); - } - - if (verificationStatus) { + await getNotificationAdapter().projectBadgeRevoked({ + project: projectWithAdmin, + }); + } else if (verificationStatus) { await getNotificationAdapter().projectVerified({ project: projectWithAdmin, }); @@ -2337,7 +2310,7 @@ export const updateStatusOfProjects = async ( request: AdminBroRequestInterface, status, ) => { - const { h, resource, records, currentAdmin } = context; + const { records, currentAdmin } = context; try { const projectStatus = await ProjectStatus.findOne({ id: status }); if (projectStatus) { @@ -2354,12 +2327,7 @@ export const updateStatusOfProjects = async ( .updateEntity(true) .execute(); - Project.sendBulkEventsToSegment( - projects.raw, - segmentProjectStatusEvents[projectStatus.symbol], - ); for (const project of projects.raw) { - await dispatchProjectUpdateEvent(project); await Project.addProjectStatusHistoryRecord({ project, status: projectStatus, diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index f1614f438..51306c8a0 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -26,10 +26,8 @@ import { getAdminBroRouter, adminBroQueryCache, } from './adminBro'; -import { initHandlingTraceCampaignUpdateEvents } from '../services/trace/traceService'; import { redis } from '../redis'; import { logger } from '../utils/logger'; -import { runUpdateTraceableProjectsTotalDonations } from '../services/cronJobs/syncTraceTotalDonationsValue'; import { runNotifyMissingDonationsCronJob } from '../services/cronJobs/notifyDonationsWithSegment'; import { errorMessages, @@ -303,9 +301,7 @@ export async function bootstrap() { runCheckPendingDonationsCronJob(); runNotifyMissingDonationsCronJob(); runCheckPendingProjectListingCronJob(); - initHandlingTraceCampaignUpdateEvents(); runUpdateDonationsWithoutValueUsdPrices(); - runUpdateTraceableProjectsTotalDonations(); if ((config.get('PROJECT_REVOKE_SERVICE_ACTIVE') as string) === 'true') { runCheckProjectVerificationStatus(); diff --git a/src/services/cronJobs/checkProjectVerificationStatus.ts b/src/services/cronJobs/checkProjectVerificationStatus.ts index d4f79c750..06b41c571 100644 --- a/src/services/cronJobs/checkProjectVerificationStatus.ts +++ b/src/services/cronJobs/checkProjectVerificationStatus.ts @@ -27,6 +27,7 @@ import { } from '../../repositories/projectVerificationRepository'; import { SegmentAnalyticsSingleton } from '../segment/segmentAnalyticsSingleton'; import { sleep } from '../../utils/utils'; +import { getNotificationAdapter } from '../../adapters/adaptersFactory'; const analytics = SegmentAnalyticsSingleton.getInstance(); @@ -171,40 +172,29 @@ const remindUpdatesOrRevokeVerification = async (project: Project) => { const user = await User.findOne({ id: Number(project.admin) }); - // segment notifications - const segmentProject = { - email: user?.email, - title: project.title, - lastName: user?.lastName, - firstName: user?.firstName, - OwnerId: user?.id, - slug: project.slug, - walletAddress: project.walletAddress, - description: project.description, - }; - - await analytics.track( - selectSegmentEvent(project.verificationStatus), - `givethId-${user?.id}`, - segmentProject, - null, - ); + await sendProperNotification(project, project.verificationStatus as string); await sleep(1000); }; -const selectSegmentEvent = projectVerificationStatus => { +const sendProperNotification = ( + project: Project, + projectVerificationStatus: string, +) => { switch (projectVerificationStatus) { case RevokeSteps.Reminder: - return NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKE_REMINDER; + return getNotificationAdapter().projectBadgeRevokeReminder({ project }); case RevokeSteps.Warning: - return NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKE_WARNING; + return getNotificationAdapter().projectBadgeRevokeWarning({ project }); case RevokeSteps.LastChance: - return NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKE_LAST_WARNING; + return getNotificationAdapter().projectBadgeRevokeLastWarning({ + project, + }); case RevokeSteps.Revoked: - return NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_REVOKED; + return getNotificationAdapter().projectBadgeRevoked({ project }); case RevokeSteps.UpForRevoking: - return NOTIFICATIONS_EVENT_NAMES.PROJECT_BADGE_UP_FOR_REVOKING; + return getNotificationAdapter().projectBadgeUpForRevoking({ project }); + default: throw new Error( i18n.__( diff --git a/src/services/cronJobs/syncProjectsRequiredForListing.ts b/src/services/cronJobs/syncProjectsRequiredForListing.ts index b8487b298..01fbccedf 100644 --- a/src/services/cronJobs/syncProjectsRequiredForListing.ts +++ b/src/services/cronJobs/syncProjectsRequiredForListing.ts @@ -5,6 +5,7 @@ import { getRepository } from 'typeorm'; import config from '../../config'; import { NOTIFICATIONS_EVENT_NAMES } from '../../analytics/analytics'; import { logger } from '../../utils/logger'; +import { getNotificationAdapter } from '../../adapters/adaptersFactory'; const cronJobTime = (config.get('MAKE_UNREVIEWED_PROJECT_LISTED_CRONJOB_EXPRESSION') as string) || @@ -35,6 +36,6 @@ export const updateProjectListing = async () => { id: project.id, listed: true, }); - Project.notifySegment(project, NOTIFICATIONS_EVENT_NAMES.PROJECT_LISTED); + await getNotificationAdapter().projectListed({ project }); } }; diff --git a/src/services/cronJobs/syncTraceTotalDonationsValue.ts b/src/services/cronJobs/syncTraceTotalDonationsValue.ts deleted file mode 100644 index cf8c435e8..000000000 --- a/src/services/cronJobs/syncTraceTotalDonationsValue.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Donation } from '../../entities/donation'; -import { schedule } from 'node-cron'; -import { fetchGivHistoricPrice } from '../givPriceService'; -import { convertExponentialNumber } from '../../utils/utils'; -import { updateTotalDonationsOfProject } from '../donationService'; -import { logger } from '../../utils/logger'; -import config from '../../config'; -import { Project } from '../../entities/project'; -import { getCampaignTotalDonationsInUsd } from '../trace/traceService'; - -// */10 * * * * means every 10 minutes -const cronJobTime = - (config.get( - 'UPDATE_TRACEBLE_PROJECT_DONATIONS_CRONJOB_EXPRESSION', - ) as string) || '*/10 * * * *'; - -export const runUpdateTraceableProjectsTotalDonations = () => { - logger.debug('runUpdateTraceableProjectsTotalDonations() has been called'); - schedule(cronJobTime, async () => { - await updateTraceableProjectsTotalDonations(); - }); -}; - -const updateTraceableProjectsTotalDonations = async () => { - logger.debug('updateTraceableProjectsTotalDonations has been called'); - const projects = await Project.createQueryBuilder('project') - .where(`"traceCampaignId" IS NOT NULL `) - .getMany(); - for (const project of projects) { - const traceCampaignId = project.traceCampaignId as string; - try { - project.totalTraceDonations = await getCampaignTotalDonationsInUsd( - traceCampaignId, - ); - await project.save(); - } catch (e) { - logger.error('Fail get trace donations of project', { - projectId: project.id, - traceCampaignId, - }); - } - } -}; diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 9a664f958..91f76a867 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -2,11 +2,7 @@ import { Project } from '../entities/project'; import { Token } from '../entities/token'; import { Donation, DONATION_STATUS } from '../entities/donation'; import { TransakOrder } from './transak/order'; -import { User } from '../entities/user'; -import DonationTracker from './segment/DonationTracker'; -import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; import { logger } from '../utils/logger'; -import { Organization } from '../entities/organization'; import { findUserById } from '../repositories/userRepository'; import { errorMessages, @@ -270,12 +266,6 @@ export const sendSegmentEventForDonation = async (params: { const donorUser = await findUserById(donation.userId); const projectOwner = project.adminUser; if (projectOwner) { - new DonationTracker( - donation, - project, - projectOwner, - NOTIFICATIONS_EVENT_NAMES.DONATION_RECEIVED, - ).track(); await getNotificationAdapter().donationReceived({ donation, project, @@ -283,12 +273,6 @@ export const sendSegmentEventForDonation = async (params: { } if (donorUser) { - new DonationTracker( - donation, - project, - donorUser, - NOTIFICATIONS_EVENT_NAMES.MADE_DONATION, - ).track(); await getNotificationAdapter().donationSent({ donation, project, diff --git a/src/services/segment/DonationTracker.test.ts b/src/services/segment/DonationTracker.test.ts deleted file mode 100644 index 51091d367..000000000 --- a/src/services/segment/DonationTracker.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { assert } from 'chai'; -import { - createProjectData, - generateRandomEtheriumAddress, - generateRandomTxHash, - saveDonationDirectlyToDb, - saveProjectDirectlyToDb, - saveUserDirectlyToDb, -} from '../../../test/testUtils'; -import { NOTIFICATIONS_EVENT_NAMES } from '../../analytics/analytics'; -import { DONATION_STATUS } from '../../entities/donation'; -import { NETWORK_IDS } from '../../provider'; -import DonationTracker from './DonationTracker'; - -describe( - 'segmentDonationAttributes() test cases', - segmentDonationAttributesTestCases, -); - -function segmentDonationAttributesTestCases() { - it('should generate donation attributes when passing a donation', async () => { - const transactionInfo = { - txHash: generateRandomTxHash(), - networkId: NETWORK_IDS.XDAI, - amount: 1, - fromAddress: generateRandomEtheriumAddress(), - toAddress: generateRandomEtheriumAddress(), - currency: 'GIV', - timestamp: 1647069070, - }; - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - nonce: 999999, - valueUsd: 1, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - const tracker = new DonationTracker( - donation, - project, - user, - NOTIFICATIONS_EVENT_NAMES.MADE_DONATION, - ); - - const segmentAttributes = tracker.segmentDonationAttributes(); - assert.equal(segmentAttributes.slug, project.slug); - /* tslint:disable:no-string-literal */ - assert.equal( - segmentAttributes['fromWalletAddress'], - transactionInfo.fromAddress, - ); - }); - - it('should generate donation attributes but remove fromWalletAddress when event is donation received', async () => { - const transactionInfo = { - txHash: generateRandomTxHash(), - networkId: NETWORK_IDS.XDAI, - amount: 1, - fromAddress: generateRandomEtheriumAddress(), - toAddress: generateRandomEtheriumAddress(), - currency: 'GIV', - timestamp: 1647069070, - }; - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); - const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const donation = await saveDonationDirectlyToDb( - { - amount: transactionInfo.amount, - transactionNetworkId: transactionInfo.networkId, - transactionId: transactionInfo.txHash, - currency: transactionInfo.currency, - fromWalletAddress: transactionInfo.fromAddress, - toWalletAddress: transactionInfo.toAddress, - nonce: 999999, - valueUsd: 1, - anonymous: false, - createdAt: new Date(transactionInfo.timestamp), - status: DONATION_STATUS.PENDING, - }, - user.id, - project.id, - ); - const tracker = new DonationTracker( - donation, - project, - user, - NOTIFICATIONS_EVENT_NAMES.DONATION_RECEIVED, - ); - - const segmentAttributes = tracker.segmentDonationAttributes(); - assert.equal(segmentAttributes.slug, project.slug); - /* tslint:disable:no-string-literal */ - assert.notEqual( - segmentAttributes['fromWalletAddress'], - transactionInfo.fromAddress, - ); - /* tslint:disable:no-string-literal */ - assert.equal(segmentAttributes['fromWalletAddress'], undefined); - }); -} diff --git a/src/services/segment/DonationTracker.ts b/src/services/segment/DonationTracker.ts deleted file mode 100644 index df03e458d..000000000 --- a/src/services/segment/DonationTracker.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { User } from '../../entities/user'; -import { Donation } from '../../entities/donation'; -import { Project } from '../../entities/project'; -import { NOTIFICATIONS_EVENT_NAMES } from '../../analytics/analytics'; -import { SegmentAnalyticsSingleton } from './segmentAnalyticsSingleton'; - -interface DonationAttributes { - email?: String; - title?: String; - firstName?: String; - projectOwnerId?: String; - slug?: String; - amount?: Number; - transactionId?: String; - transactionNetworkId?: Number; - currency?: String; - createdAt?: Date; - toWalletAddress?: String; - fromWalletAddress?: String; - donationValueUsd?: Number; - donationValueEth?: Number; - verified?: Boolean; - transakStatus?: String; -} - -/** - * Notifies Segment any event concerning the donation - */ -class DonationTracker { - donation: Donation; - eventName: NOTIFICATIONS_EVENT_NAMES; - project: Project; - user: User; - - constructor( - donationToUpdate: Donation, - projectToNotify: Project, - userToNotify: User, - eventTitle: NOTIFICATIONS_EVENT_NAMES, - ) { - this.donation = donationToUpdate; - this.project = projectToNotify; - this.user = userToNotify; - this.eventName = eventTitle; - } - - track() { - SegmentAnalyticsSingleton.getInstance().track( - this.eventName, - this.user.segmentUserId(), - this.segmentDonationAttributes(), - this.user.segmentUserId(), - ); - } - - // it's partial because anonymous has less values - segmentDonationAttributes(): Partial { - const donationAttributes = { - email: this.user.email, - title: this.project.title, - firstName: this.user.firstName, - projectOwnerId: this.project.admin, - slug: this.project.slug, - amount: Number(this.donation.amount), - transactionId: this.donation.transactionId.toLowerCase(), - transactionNetworkId: Number(this.donation.transactionNetworkId), - currency: this.donation.currency, - createdAt: new Date(), - toWalletAddress: this.donation.toWalletAddress.toLowerCase(), - fromWalletAddress: this.donation.fromWalletAddress.toLowerCase(), - donationValueUsd: this.donation.valueUsd, - donationValueEth: this.donation.valueEth, - verified: Boolean(this.project.verified), - transakStatus: this.donation.transakStatus, - } as Partial; - - if (this.eventName === NOTIFICATIONS_EVENT_NAMES.DONATION_RECEIVED) { - delete donationAttributes.fromWalletAddress; - } - - return donationAttributes; - } -} - -export default DonationTracker; diff --git a/src/services/segment/projectTracker.ts b/src/services/segment/projectTracker.ts deleted file mode 100644 index 4cae7514c..000000000 --- a/src/services/segment/projectTracker.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { User } from '../../entities/user'; -import { Project } from '../../entities/project'; -import { NOTIFICATIONS_EVENT_NAMES } from '../../analytics/analytics'; -import { findUserById } from '../../repositories/userRepository'; -import { SegmentAnalyticsSingleton } from './segmentAnalyticsSingleton'; - -/** - * Notifies Segment any event concerning the project - */ -class ProjectTracker { - project: Project; - eventName: NOTIFICATIONS_EVENT_NAMES; - projectOwner?: User; - - constructor(projectToUpdate: Project, eventTitle: NOTIFICATIONS_EVENT_NAMES) { - this.project = projectToUpdate; - this.eventName = eventTitle; - } - - async track() { - this.projectOwner = await findUserById(Number(this.project.admin)); - if (this.projectOwner) { - SegmentAnalyticsSingleton.getInstance().track( - this.eventName, - this.projectOwner.segmentUserId(), - this.segmentProjectAttributes(), - null, - ); - } - } - - private segmentProjectAttributes() { - return { - email: this.projectOwner?.email, - title: this.project.title, - lastName: this.projectOwner?.lastName, - firstName: this.projectOwner?.firstName, - OwnerId: this.projectOwner?.id, - slug: this.project.slug, - walletAddress: this.project.walletAddress, - }; - } -} - -export default ProjectTracker; diff --git a/src/services/trace/traceService.ts b/src/services/trace/traceService.ts deleted file mode 100644 index 424f84140..000000000 --- a/src/services/trace/traceService.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { Project, ProjStatus } from '../../entities/project'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../../utils/errorMessages'; -import { ProjectStatus } from '../../entities/projectStatus'; -import { RedisOptions } from 'ioredis'; -import { logger } from '../../utils/logger'; -import axios from 'axios'; -import { NETWORK_IDS } from '../../provider'; -// tslint:disable-next-line:no-var-requires -const Queue = require('bull'); - -const TWO_MINUTES = 1000 * 60 * 2; - -// There is shared redis between giveth.io and trace.giveth.io notify each other about verifiedCampaigns/project update -const redisConfig: RedisOptions = { - host: process.env.SHARED_REDIS_HOST, - port: Number(process.env.SHARED_REDIS_PORT), -}; -if (process.env.SHARED_REDIS_PASSWORD) { - redisConfig.password = process.env.SHARED_REDIS_PASSWORD; -} - -const updateCampaignQueue = new Queue('trace-campaign-updated', { - redis: redisConfig, -}); -const updateGivethIoProjectQueue = new Queue('givethio-project-updated', { - redis: redisConfig, -}); - -updateCampaignQueue.on('error', err => { - logger.error('updateCampaignQueue connection error', err); -}); -updateGivethIoProjectQueue.on('error', err => { - logger.error('updateGivethIoProjectQueue connection error', err); -}); - -setInterval(async () => { - const updateCampaignQueueCount = await updateCampaignQueue.count(); - const updateGivethIoProjectQueueCount = - await updateGivethIoProjectQueue.count(); - logger.info(`Sync trace and givethio job queues count:`, { - updateCampaignQueueCount, - updateGivethIoProjectQueueCount, - }); -}, TWO_MINUTES); - -export interface UpdateCampaignData { - title: string; - campaignId?: string; - description: string; - verified?: boolean; - archived?: boolean; -} - -export const dispatchProjectUpdateEvent = async ( - project: Project, -): Promise => { - try { - if (!project.traceCampaignId) { - logger.debug( - 'updateCampaignInTrace(), the project is not a trace campaign', - { - projectId: project.id, - }, - ); - return; - } - const payload: UpdateCampaignData = { - campaignId: project.traceCampaignId, - title: project.title, - description: project.description as string, - verified: project.verified, - archived: project.statusId === ProjStatus.cancelled, - }; - - logger.debug('dispatchProjectUpdateEvent() add event to queue', payload); - // Giveth trace will handle this event - await updateGivethIoProjectQueue.add(payload); - } catch (e) { - logger.error('updateCampaignInTrace() error', { - e, - project, - }); - } -}; - -export const initHandlingTraceCampaignUpdateEvents = () => { - updateCampaignQueue.process(1, async (job, done) => { - logger.debug('Listen to events of ', updateCampaignQueue.name); - - // These events come from Giveth trace - try { - const { givethIoProjectId, campaignId, status, title, description } = - job.data; - logger.debug('updateGivethIoProjectQueue(), job.data', job.data); - const project = await Project.findOne(givethIoProjectId); - if (!project) { - throw new Error( - i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND), - ); - } - project.traceCampaignId = campaignId; - project.isImported = true; - project.title = title; - project.description = description; - let statusId; - if (status === 'Archived') { - statusId = ProjStatus.cancelled; - } else if ( - status === 'Active' && - project.status.id === ProjStatus.cancelled - ) { - // Maybe project status is deactive in giveth.io, so we should not - // change to active in this case, we just change the cancel status to active with this endpoint - statusId = ProjStatus.active; - } - if (statusId) { - const projectStatus = (await ProjectStatus.findOne({ - id: statusId, - })) as ProjectStatus; - project.status = projectStatus; - } - - await project.save(); - done(); - } catch (e) { - logger.error('updateGivethIoProjectQueue() error', e); - done(); - } - }); -}; - -export const getCampaignTotalDonationsInUsd = async ( - campaignId: string, -): Promise => { - const url = `${process.env.GIVETH_TRACE_BASE_URL}/campaignTotalDonationValue/${campaignId}`; - try { - const result = await axios.get(url); - return result.data.totalUsdValue; - } catch (e) { - logger.error('getCampaignTotalDonationsInUsd error', e); - throw e; - } -}; - -interface TraceDonationInterface { - status: string; - usdValue: number; - amount: string; - giverAddress: string; - homeTxHash: string; - txHash: string; - token: { - symbol: string; - - // It is incorrect value - // decimals: number; - }; - campaignId: string; - createdAt: string; -} - -export const getCampaignDonations = async (input: { - campaignId: string; - take: number; - skip: number; -}): Promise<{ - total: number; - donations: [ - { - createdAt: Date; - amount: number; - currency: string; - valueUsd: number; - transactionId: string; - transactionNetworkId: number; - fromWalletAddress: string; - }, - ]; -}> => { - const { campaignId, take, skip } = input; - const url = `${process.env.GIVETH_TRACE_BASE_URL}/donations`; - // For see how REST calls of feathers application should be, see https://docs.feathersjs.com/api/client/rest.html#find - const urlWithQueryString = `${url}?$sort[createdAt]=-1&status[$in]=Committed&status[$in]=Waiting&ownerTypeId=${campaignId}&$limit=${take}$$skip=${skip}`; - // Query inspired by https://github.com/Giveth/feathers-giveth/blob/2d990df8e87087f8da0e70146a5adb4f41ab7f75/src/services/aggregateDonations/aggregateDonations.service.js#L23-L39 - try { - const result = await axios.get(urlWithQueryString); - const donations = result.data.data.map( - (traceDonation: TraceDonationInterface) => { - return { - createdAt: new Date(traceDonation.createdAt), - amount: Number(traceDonation.amount) / 10 ** 18, - currency: traceDonation.token.symbol, - valueUsd: traceDonation.usdValue, - - // Currently trace just support mainnet donations - transactionNetworkId: NETWORK_IDS.MAIN_NET, - - transactionId: traceDonation.homeTxHash || traceDonation.txHash, - fromWalletAddress: traceDonation.giverAddress, - }; - }, - ); - return { - total: result.data.total, - donations, - }; - } catch (e) { - logger.error('getCampaignDonations error', e); - throw e; - } -};