Skip to content

Commit

Permalink
Send Notifications to donors and people who liked the projects
Browse files Browse the repository at this point in the history
related to #699
  • Loading branch information
mohammadranjbarz committed Oct 25, 2022
1 parent 0406323 commit 03330f2
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 13 deletions.
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
5 changes: 5 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,8 +30,12 @@ export class Reaction extends BaseEntity {
@Column({ nullable: true })
projectUpdateId: number;

@Field(type => User, { nullable: true })
@ManyToOne(type => User, { nullable: true })
user: User;
@Field(type => ID)
@Column()
@RelationId((reaction: Reaction) => reaction.user)
userId: number;

@Field(type => String)
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),
);
});
}
20 changes: 20 additions & 0 deletions src/repositories/donationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,23 @@ export const findDonationById = async (
.leftJoinAndSelect('donation.project', 'project')
.getOne();
};

export const findUsersWhoDonatedToProject = async (
projectId: number,
): Promise<{ walletAddress: string; email?: string }[]> => {
const donations = await Donation.createQueryBuilder('donation')
.leftJoin('donation.user', 'user')
.addSelect(['user.walletAddress', 'user.email'])
.distinctOn(['user.walletAddress'])
.where(`"projectId"=:projectId`, {
projectId,
})
.getMany();

return donations.map(donation => {
return {
walletAddress: donation.user.walletAddress?.toLowerCase() as string,
email: donation.user.email,
};
});
};
Loading

0 comments on commit 03330f2

Please sign in to comment.