Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send Notifications to donors and people who liked the projects #700

Merged
merged 2 commits into from
Oct 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"test:userRepository": "NODE_ENV=test mocha -t 30000 --exit -r ts-node/register ./test/pre-test-scripts.ts ./src/repositories/userRepository.test.ts",
"test:statusReasonRepository": "NODE_ENV=test mocha -t 30000 --exit -r ts-node/register ./test/pre-test-scripts.ts ./src/repositories/statusReasonRepository.test.ts",
"test:donationRepository": "NODE_ENV=test mocha -t 30000 --exit -r ts-node/register ./test/pre-test-scripts.ts ./src/repositories/donationRepository.test.ts",
"test:reactionRepository": "NODE_ENV=test mocha -t 30000 --exit -r ts-node/register ./test/pre-test-scripts.ts ./src/repositories/reactionRepository.test.ts",
"test:socialProfileRepository": "NODE_ENV=test mocha -t 30000 --exit -r ts-node/register ./test/pre-test-scripts.ts ./src/repositories/socialProfileRepository.test.ts",
"test:powerBalanceSnapshotRepository": "NODE_ENV=test mocha -t 30000 --exit -r ts-node/register ./test/pre-test-scripts.ts ./src/repositories/powerBalanceSnapshotRepository.test.ts",
"test:projectVerificationRepository": "NODE_ENV=test mocha -t 30000 --exit -r ts-node/register ./test/pre-test-scripts.ts ./src/repositories/projectVerificationRepository.test.ts",
Expand Down
155 changes: 143 additions & 12 deletions src/adapters/notifications/NotificationCenterAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,39 @@ import { User } from '../../entities/user';
import { createBasicAuthentication } from '../../utils/utils';
import { logger } from '../../utils/logger';
import { NOTIFICATIONS_EVENT_NAMES } from '../../analytics/analytics';
import Bull from 'bull';
import { redisConfig } from '../../redis';
import config from '../../config';
import { findUsersWhoDonatedToProject } from '../../repositories/donationRepository';
import { findUsersWhoLikedProject } from '../../repositories/reactionRepository';
const notificationCenterUsername = process.env.NOTIFICATION_CENTER_USERNAME;
const notificationCenterPassword = process.env.NOTIFICATION_CENTER_PASSWORD;
const notificationCenterBaseUrl = process.env.NOTIFICATION_CENTER_BASE_URL;

const numberOfSendNotificationsConcurrentJob =
Number(
config.get('NUMBER_OF_FILLING_POWER_SNAPSHOT_BALANCE_CONCURRENT_JOB'),
) || 30;

interface ProjectRelatedNotificationsQueue {
project: Project;
eventName: NOTIFICATIONS_EVENT_NAMES;
metadata?: any;
user?: {
walletAddress: string;
email?: string;
};
}

const sendProjectRelatedNotificationsBalanceQueue =
new Bull<ProjectRelatedNotificationsQueue>(
'send-project-related-notifications',
{
redis: redisConfig,
},
);
let isProcessingQueueEventsEnabled = false;

interface SendNotificationBody {
sendEmail?: boolean;
eventName: string;
Expand All @@ -27,6 +56,38 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface {
userName: notificationCenterUsername,
password: notificationCenterPassword,
});
if (!isProcessingQueueEventsEnabled) {
// We send notifications to project owners immediately, but as donors and people
// who liked project can be thousands or more we enqueue them and send it by that to manage
// load on notification-center and make sure all of notifications would arrive
this.processSendingNotifications();
isProcessingQueueEventsEnabled = true;
}
}

processSendingNotifications() {
logger.debug('processSendingNotifications() has been called ', {
numberOfSendNotificationsConcurrentJob,
});
sendProjectRelatedNotificationsBalanceQueue.process(
numberOfSendNotificationsConcurrentJob,
async (job, done) => {
logger.debug('processing send notification job', job.data);
const { project, metadata, eventName, user } = job.data;
try {
await this.sendProjectRelatedNotification({
project,
eventName,
metadata,
user,
});
} catch (e) {
logger.error('processSendingNotifications >> error', e);
} finally {
done();
}
},
);
}

async donationReceived(params: {
Expand Down Expand Up @@ -78,33 +139,98 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface {
return Promise.resolve(undefined);
}

projectCancelled(params: { project: Project }): Promise<void> {
async projectCancelled(params: { project: Project }): Promise<void> {
const { project } = params;
return this.sendProjectRelatedNotification({

const donors = await findUsersWhoDonatedToProject(project.id);
donors.map(user =>
sendProjectRelatedNotificationsBalanceQueue.add({
project,
eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_CANCELLED_DONORS,
user,
}),
);

const usersWhoLiked = await findUsersWhoLikedProject(project.id);
usersWhoLiked.map(user =>
sendProjectRelatedNotificationsBalanceQueue.add({
project,
eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_CANCELLED_USERS_WHO_LIKED,
user,
}),
);

await this.sendProjectRelatedNotification({
project,
eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_CANCELLED,
});
}

projectDeListed(params: { project: Project }): Promise<void> {
async projectDeListed(params: { project: Project }): Promise<void> {
const { project } = params;
return this.sendProjectRelatedNotification({

const donors = await findUsersWhoDonatedToProject(project.id);
donors.map(user =>
sendProjectRelatedNotificationsBalanceQueue.add({
project,
eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UNLISTED_DONORS,
user,
}),
);

const usersWhoLiked = await findUsersWhoLikedProject(project.id);
usersWhoLiked.map(user =>
sendProjectRelatedNotificationsBalanceQueue.add({
project,
eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UNLISTED_USERS_WHO_LIKED,
user,
}),
);
await this.sendProjectRelatedNotification({
project,
eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_UNLISTED,
});
}

projectDeactivated(params: { project: Project }): Promise<void> {
const { project } = params;
return this.sendProjectRelatedNotification({
async projectDeactivated(params: {
project: Project;
reason?: string;
}): Promise<void> {
const { project, reason } = params;
const metadata = {
reason,
};
const donors = await findUsersWhoDonatedToProject(project.id);
donors.map(user =>
sendProjectRelatedNotificationsBalanceQueue.add({
project,
eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_DEACTIVATED_DONORS,
user,
metadata,
}),
);

const usersWhoLiked = await findUsersWhoLikedProject(project.id);
usersWhoLiked.map(user =>
sendProjectRelatedNotificationsBalanceQueue.add({
project,
eventName:
NOTIFICATIONS_EVENT_NAMES.PROJECT_DEACTIVATED_USERS_WHO_LIKED,
user,
metadata,
}),
);
await this.sendProjectRelatedNotification({
project,
eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_DEACTIVATED,
metadata,
});
}

projectListed(params: { project: Project }): Promise<void> {
async projectListed(params: { project: Project }): Promise<void> {
const { project } = params;
return this.sendProjectRelatedNotification({

await this.sendProjectRelatedNotification({
project,
eventName: NOTIFICATIONS_EVENT_NAMES.PROJECT_LISTED,
});
Expand Down Expand Up @@ -146,14 +272,19 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface {
project: Project;
eventName: NOTIFICATIONS_EVENT_NAMES;
metadata?: any;
user?: {
walletAddress: string;
email?: string;
};
}): Promise<void> {
const { project, eventName, metadata } = params;
const { project, eventName, metadata, user } = params;
const receivedUser = user || (project.adminUser as User);
return this.callSendNotification({
eventName,
email: project.adminUser?.email,
email: receivedUser.email,
// currently Segment handle sending emails, so notification-center doesnt need to send that
sendEmail: false,
userWalletAddress: String(project.adminUser?.walletAddress),
userWalletAddress: receivedUser.walletAddress as string,
projectId: String(project.id),
metadata: {
projectTitle: project.title,
Expand Down
9 changes: 8 additions & 1 deletion src/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export enum NOTIFICATIONS_EVENT_NAMES {
DRAFTED_PROJECT_ACTIVATED = 'Draft published',
PROJECT_LISTED = 'Project listed',
PROJECT_UNLISTED = 'Project unlisted',
PROJECT_UNLISTED_DONORS = 'Project unlisted - Donors',
PROJECT_UNLISTED_USERS_WHO_LIKED = 'Project unlisted - Users Who Liked',
PROJECT_EDITED = 'Project edited',
PROJECT_BADGE_REVOKED = 'Project badge revoked',
PROJECT_BADGE_REVOKE_REMINDER = 'Project badge revoke reminder',
Expand All @@ -20,14 +22,19 @@ export enum NOTIFICATIONS_EVENT_NAMES {
PROJECT_UNVERIFIED = 'Project unverified',
PROJECT_ACTIVATED = 'Project activated',
PROJECT_DEACTIVATED = 'Project deactivated',
PROJECT_DEACTIVATED_DONORS = 'Project deactivated - Donors',
PROJECT_DEACTIVATED_USERS_WHO_LIKED = 'Project deactivated - Users Who Liked',

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',
PROJECT_RECEIVED_HEART = 'project liked',
PROJECT_UPDATED_DONOR = 'Project updated - donor',
PROJECT_UPDATED_OWNER = 'Project updated - owner',
PROJECT_CREATED = 'Project created',
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',
Expand Down
4 changes: 4 additions & 0 deletions src/entities/reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Index,
} from 'typeorm';
import { Project, ProjectUpdate } from './project';
import { User } from './user';

@Entity()
@ObjectType()
Expand All @@ -29,6 +30,9 @@ export class Reaction extends BaseEntity {
@Column({ nullable: true })
projectUpdateId: number;

// We just fill it with join when making query so dont need to Add @Column or @ManyToOne
user: User;

@Field(type => ID)
@Column()
userId: number;
Expand Down
53 changes: 53 additions & 0 deletions src/repositories/donationRepository.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
generateRandomTxHash,
saveDonationDirectlyToDb,
saveProjectDirectlyToDb,
saveUserDirectlyToDb,
SEED_DATA,
} from '../../test/testUtils';
import { User, UserRole } from '../entities/user';
Expand All @@ -13,6 +14,7 @@ import {
createDonation,
findDonationById,
findDonationsByTransactionId,
findUsersWhoDonatedToProject,
} from './donationRepository';

describe('createDonation test cases', () => {
Expand Down Expand Up @@ -55,6 +57,10 @@ describe(
findDonationsByTransactionIdTestCases,
);
describe('findDonationById() test cases', findDonationByIdTestCases);
describe(
'findUsersWhoDonatedToProject() test cases',
findUsersWhoDonatedToProjectTestCases,
);

function findDonationsByTransactionIdTestCases() {
it('should return donation with txHash ', async () => {
Expand Down Expand Up @@ -119,3 +125,50 @@ function findDonationByIdTestCases() {
assert.isNotOk(fetchedDonation);
});
}

function findUsersWhoDonatedToProjectTestCases() {
it('should find wallet addresses of who donated to a project', async () => {
const project = await saveProjectDirectlyToDb(createProjectData());
const donor1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
const donor2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
const donor3 = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
await saveDonationDirectlyToDb(createDonationData(), donor1.id, project.id);
await saveDonationDirectlyToDb(createDonationData(), donor2.id, project.id);
await saveDonationDirectlyToDb(createDonationData(), donor3.id, project.id);
const users = await findUsersWhoDonatedToProject(project.id);
assert.equal(users.length, 3);
assert.isOk(
users.find(user => user.walletAddress === donor1.walletAddress),
);
assert.isOk(
users.find(user => user.walletAddress === donor2.walletAddress),
);
assert.isOk(
users.find(user => user.walletAddress === donor3.walletAddress),
);
});
it('should find wallet addresses of who donated to a project, not include repetitive items', async () => {
const project = await saveProjectDirectlyToDb(createProjectData());
const donor1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
const donor2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
const donor3 = await saveUserDirectlyToDb(generateRandomEtheriumAddress());
await saveDonationDirectlyToDb(createDonationData(), donor1.id, project.id);
await saveDonationDirectlyToDb(createDonationData(), donor1.id, project.id);
await saveDonationDirectlyToDb(createDonationData(), donor1.id, project.id);
await saveDonationDirectlyToDb(createDonationData(), donor2.id, project.id);
await saveDonationDirectlyToDb(createDonationData(), donor2.id, project.id);
await saveDonationDirectlyToDb(createDonationData(), donor3.id, project.id);
await saveDonationDirectlyToDb(createDonationData(), donor3.id, project.id);
const users = await findUsersWhoDonatedToProject(project.id);
assert.equal(users.length, 3);
assert.isOk(
users.find(user => user.walletAddress === donor1.walletAddress),
);
assert.isOk(
users.find(user => user.walletAddress === donor2.walletAddress),
);
assert.isOk(
users.find(user => user.walletAddress === donor3.walletAddress),
);
});
}
13 changes: 13 additions & 0 deletions src/repositories/donationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,16 @@ export const findDonationById = async (
.leftJoinAndSelect('donation.project', 'project')
.getOne();
};

export const findUsersWhoDonatedToProject = async (
projectId: number,
): Promise<{ walletAddress: string; email?: string }[]> => {
return Donation.createQueryBuilder('donation')
.leftJoin('donation.user', 'user')
.distinctOn(['user.walletAddress'])
.select('LOWER(user.walletAddress) AS "walletAddress", user.email as email')
.where(`"projectId"=:projectId`, {
projectId,
})
.getRawMany();
};
Loading