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

Implement sending bulk notifications #26

Merged
merged 4 commits into from
Dec 29, 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: 0 additions & 1 deletion migrations/1660539566228-createUserAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
MigrationInterface,
QueryRunner,
Table,
TableForeignKey,
} from 'typeorm';

export class createUserAddress1660539566228 implements MigrationInterface {
Expand Down
1 change: 0 additions & 1 deletion migrations/1660540253547-createNotificationType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
MigrationInterface,
QueryRunner,
Table,
TableForeignKey,
TableIndex,
} from 'typeorm';

Expand Down
9 changes: 5 additions & 4 deletions migrations/1660716115917-seedNotificationType.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Column, MigrationInterface, QueryRunner} from 'typeorm';
import { MigrationInterface, QueryRunner } from 'typeorm';
import {
NotificationType,
SCHEMA_VALIDATORS_NAMES,
Expand Down Expand Up @@ -1662,7 +1662,6 @@ export const GivethNotificationTypes = {
type: 'p',
content: ' unlocked.',
},

],
content: '{amount} unlocked.',
},
Expand Down Expand Up @@ -1766,7 +1765,8 @@ export const GivethNotificationTypes = {
PROJECT_STATUS_GROUP: {
name: 'Project status',
title: 'Your project status',
description: 'When your own project has been listed, unlisted, cancelled, activated or deactivated',
description:
'When your own project has been listed, unlisted, cancelled, activated or deactivated',
showOnSettingPage: true,
isEmailEditable: true,
isWebEditable: false,
Expand Down Expand Up @@ -1863,7 +1863,8 @@ export const GivethNotificationTypes = {
LIKED_BY_YOU_PROJECT_GROUP: {
name: 'Project status - Users Who Liked',
title: 'Your liked project status',
description: 'When your liked Project has been listed, unlisted, cancelled, activated or deactivated',
description:
'When your liked Project has been listed, unlisted, cancelled, activated or deactivated',
showOnSettingPage: true,
microService: MICRO_SERVICES.givethio,
schemaValidator: null,
Expand Down
6 changes: 5 additions & 1 deletion src/adapters/jwtAuthentication/mockJwtAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { JwtAuthenticationInterface } from './JwtAuthenticationInterface';
import { decode, JwtPayload } from 'jsonwebtoken';
import { errorMessages } from '../../utils/errorMessages';
import { logger } from '../../utils/logger';
import { StandardError } from '../../types/StandardError';

export class MockJwtAdapter implements JwtAuthenticationInterface {
verifyJwt(token: string): Promise<string> {
try {
const decodedJwt = decode(token) as JwtPayload;
if (!decodedJwt.publicAddress) {
throw new Error(errorMessages.UN_AUTHORIZED);
throw new StandardError({
message: errorMessages.UN_AUTHORIZED,
httpStatusCode: 401,
});
}
return decodedJwt.publicAddress.toLowerCase();
} catch (e: any) {
Expand Down
7 changes: 5 additions & 2 deletions src/controllers/v1/notificationSettingsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
import { UserAddress } from '../../entities/userAddress';
import { errorMessages } from '../../utils/errorMessages';
import {
sendNotificationValidator,
updateNotificationSettings,
updateOneNotificationSetting,
validateWithJoiSchema,
} from '../../validators/schemaValidators';
import { StandardError } from '../../types/StandardError';

interface SettingParams {
id: number;
Expand Down Expand Up @@ -86,7 +86,10 @@ export class NotificationSettingsController {
const notificationSetting = await findNotificationSettingById(id);

if (!notificationSetting) {
throw new Error(errorMessages.NOTIFICATION_SETTING_NOT_FOUND);
throw new StandardError({
message: errorMessages.NOTIFICATION_SETTING_NOT_FOUND,
httpStatusCode: 400,
});
}
const newSettingData = {
notificationSettingId: id,
Expand Down
178 changes: 58 additions & 120 deletions src/controllers/v1/notificationsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,36 @@ import {
Query,
Put,
Path,
Example,
} from 'tsoa';
import { logger } from '../../utils/logger';

import {
countUnreadValidator,
sendBulkNotificationValidator,
sendNotificationValidator,
validateWithJoiSchema,
} from '../../validators/schemaValidators';
import {
CountUnreadNotificationsResponse,
GetNotificationsResponse,
ReadAllNotificationsRequestType,
ReadAllNotificationsResponse,
ReadSingleNotificationResponse,
SendNotificationRequest,
SendNotificationResponse,
SendNotificationTypeRequest,
SendNotificationRequest,
sendBulkNotificationRequest,
} from '../../types/requestResponses';
import { errorMessages, errorMessagesEnum } from '../../utils/errorMessages';
import { StandardError } from '../../types/StandardError';
import { User } from '../../types/general';
import { errorMessages } from '../../utils/errorMessages';
import {
countUnreadNotifications,
createNotification,
findNotificationByTrackId,
getNotifications,
markNotificationGroupAsRead,
markNotificationsAsRead,
} from '../../repositories/notificationRepository';
import { UserAddress } from '../../entities/userAddress';
import {
getNotificationTypeByEventName,
getNotificationTypeByEventNameAndMicroservice,
} from '../../repositories/notificationTypeRepository';
import { EMAIL_STATUSES, Notification } from '../../entities/notification';
import { Notification } from '../../entities/notification';
import { createNewUserAddressIfNotExists } from '../../repositories/userAddressRepository';
import { SEGMENT_METADATA_SCHEMA_VALIDATOR } from '../../utils/validators/segmentAndMetadataValidators';
import { findNotificationSettingByNotificationTypeAndUserAddress } from '../../repositories/notificationSettingRepository';
import { SegmentAnalyticsSingleton } from '../../services/segment/segmentAnalyticsSingleton';
import { sendNotification } from '../../services/notificationService';
import { StandardError } from '../../types/StandardError';

@Route('/v1')
@Tags('Notification')
Expand All @@ -57,130 +47,75 @@ export class NotificationsController {
@Security('basicAuth')
public async sendNotification(
@Body()
body: SendNotificationTypeRequest,
body: SendNotificationRequest,
@Inject()
params: {
microService: string;
},
): Promise<SendNotificationResponse> {
const { microService } = params;
const { userWalletAddress, projectId } = body;
try {
validateWithJoiSchema(body, sendNotificationValidator);
if (body.trackId && (await findNotificationByTrackId(body.trackId))) {
// We dont throw error in this case but dont create new notification neither
return {
success: true,
message: errorMessages.DUPLICATED_TRACK_ID,
};
}
const userAddress = await createNewUserAddressIfNotExists(
userWalletAddress as string,
);
const notificationType =
await getNotificationTypeByEventNameAndMicroservice({
eventName: body.eventName,
microService,
});

if (!notificationType) {
throw new Error(errorMessages.INVALID_NOTIFICATION_TYPE);
}
const notificationSetting =
await findNotificationSettingByNotificationTypeAndUserAddress({
notificationTypeId: notificationType.id,
userAddressId: userAddress.id,
});

logger.debug('notificationController.sendNotification()', {
notificationSetting,
notificationType,
return sendNotification(body, microService);
// add if and logic for push notification (not in mvp)
} catch (e) {
logger.error('sendNotification() error', {
error: e,
requestBody: body,
segmentPayload: body?.segment?.payload,
});
throw e;
}
}

const shouldSendEmail =
body.sendEmail && notificationSetting?.allowEmailNotification;
let emailStatus = shouldSendEmail
? EMAIL_STATUSES.WAITING_TO_BE_SEND
: EMAIL_STATUSES.NO_NEED_TO_SEND;

const segmentValidator =
SEGMENT_METADATA_SCHEMA_VALIDATOR[
notificationType?.schemaValidator as string
]?.segment;

if (shouldSendEmail && body.sendSegment && segmentValidator) {
//TODO Currently sending email and segment event are tightly coupled, we can't send segment event without sending email
// And it's not good, we should find another solution to separate sending segment and email
const segmentData = body.segment?.payload;
validateWithJoiSchema(segmentData, segmentValidator);
await SegmentAnalyticsSingleton.getInstance().track({
eventName: notificationType.emailNotificationId as string,
anonymousId: body?.segment?.anonymousId,
properties: segmentData,
analyticsUserId: body?.segment?.analyticsUserId,
@Post('/thirdParty/notificationsBulk')
@Security('basicAuth')
public async sendBulkNotification(
@Body()
body: sendBulkNotificationRequest,
@Inject()
params: {
microService: string;
},
): Promise<SendNotificationResponse> {
const { microService } = params;
try {
validateWithJoiSchema(body, sendBulkNotificationValidator);
const trackIdsSet = new Set(
body.notifications.map(notification => notification.trackId),
);
if (trackIdsSet.size !== body.notifications.length) {
throw new StandardError({
message: errorMessages.THERE_IS_SOME_ITEMS_WITH_SAME_TRACK_ID,
httpStatusCode: 400,
});
emailStatus = EMAIL_STATUSES.SENT;
}

const metadataValidator =
SEGMENT_METADATA_SCHEMA_VALIDATOR[
notificationType?.schemaValidator as string
]?.metadata;

if (metadataValidator) {
validateWithJoiSchema(body.metadata, metadataValidator);
}

if (!notificationSetting?.allowDappPushNotification) {
//TODO In future we can add a create notification but with disabledNotification:true
// So we can exclude them in list of notifications
return {
success: true,
message: errorMessages.USER_TURNED_OF_THIS_NOTIFICATION_TYPE,
};
}
const notificationData: Partial<Notification> = {
notificationType,
userAddress,
email: body.email,
emailStatus,
trackId: body?.trackId,
metadata: body?.metadata,
segmentData: body.segment,
projectId,
};
if (body.creationTime) {
// creationTime is optional and it's timestamp in milliseconds format
notificationData.createdAt = new Date(body.creationTime);
}
await createNotification(notificationData);

await Promise.all(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will happen if two events have the same trackId? We have made that field unique in the schema, so normally it must throw an error! but the problem is that some notifications will be created and some not!
@mohammadranjbarz Can you write a test to examine it?
If you agree with to change it, the best way in my view is to put all tackIds in a Set and compare its size with the notifications size.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, only duplicates in current notifications may cause a problem. If we have a trackId saved before this request, it will only gracefully ignored.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aminlatifi , your suggestion was good, I did it.

body.notifications.map(item => sendNotification(item, microService)),
);
return { success: true };
// add if and logic for push notification (not in mvp)
} catch (e) {
logger.error('sendNotification() error', {
logger.error('sendBulkNotification() error', {
error: e,
requestBody: body,
segmentPayload: body?.segment?.payload,
});
throw e;
}
}

// https://tsoa-community.github.io/docs/examples.html#parameter-examples
/**
* @example limit "20"
* @example offset "0"
* @example category ""
* @example category "projectRelated"
* @example category "givEconomyRelated"
* @example category "general"
* @example isRead ""
* @example isRead "false"
* @example isRead "true"
* @example startTime "1659356987"

*/
* @example limit "20"
* @example offset "0"
* @example category ""
* @example category "projectRelated"
* @example category "givEconomyRelated"
* @example category "general"
* @example isRead ""
* @example isRead "false"
* @example isRead "true"
* @example startTime "1659356987"

*/
@Get('/notifications/')
@Security('JWT')
public async getNotifications(
Expand Down Expand Up @@ -258,7 +193,10 @@ export class NotificationsController {
userAddressId: user.id,
});
if (!notification) {
throw new Error(errorMessages.NOTIFICATION_NOT_FOUND);
throw new StandardError({
message: errorMessages.NOTIFICATION_NOT_FOUND,
httpStatusCode: 404,
});
}
return {
notification,
Expand Down
10 changes: 7 additions & 3 deletions src/repositories/notificationSettingRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NotificationSetting } from '../entities/notificationSetting';
import { errorMessages } from '../utils/errorMessages';
import { createQueryBuilder } from 'typeorm';
import { logger } from '../utils/logger';
import { StandardError } from '../types/StandardError';

export const createNotificationSettingsForNewUser = async (
user: UserAddress,
Expand Down Expand Up @@ -120,9 +121,12 @@ export const updateUserNotificationSetting = async (params: {
)
.getOne();

if (!notificationSetting)
throw new Error(errorMessages.NOTIFICATION_SETTING_NOT_FOUND);

if (!notificationSetting) {
throw new StandardError({
message: errorMessages.NOTIFICATION_SETTING_NOT_FOUND,
httpStatusCode: 400,
});
}
if (notificationSetting.notificationType?.isEmailEditable) {
notificationSetting.allowEmailNotification = params.allowEmailNotification;
}
Expand Down
Loading