Skip to content

Commit

Permalink
Merge pull request #113 from alphamanuscript/86-email-notifications
Browse files Browse the repository at this point in the history
Implement email service
  • Loading branch information
habbes authored Aug 28, 2020
2 parents 144685b + 8069ad0 commit 44ab618
Show file tree
Hide file tree
Showing 12 changed files with 111 additions and 2 deletions.
1 change: 1 addition & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
},
"dependencies": {
"@hapi/joi": "^17.1.1",
"@sendgrid/mail": "^7.2.4",
"africastalking": "^0.4.5",
"argon2": "^0.26.2",
"axios": "^0.19.2",
Expand Down
6 changes: 5 additions & 1 deletion server/src/core/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ export interface AppConfig {
* Google API Client ID
*/
googleClientId: string;
sendgridApiKey: string;
emailSender: string;
};

export function loadAppConfigFromEnv(env: { [key: string]: string }): AppConfig {
Expand All @@ -133,6 +135,8 @@ export function loadAppConfigFromEnv(env: { [key: string]: string }): AppConfig
distributionPeriodLength: (env.DISTRIBUTION_PERIOD_LENGTH && Number(env.DISTRIBUTION_PERIOD_LENGTH)) || 30,
distributionInterval: (env.DISTRIBUTION_INTERVAL && Number(env.DISTRIBUTION_INTERVAL)) || 1,
statsComputationInterval: (env.STATS_COMPUTATION_INTERVAL && Number(env.STATS_COMPUTATION_INTERVAL)) || 1,
googleClientId: env.GOOGLE_CLIENT_ID
googleClientId: env.GOOGLE_CLIENT_ID,
sendgridApiKey: env.SENDGRID_API_KEY || '',
emailSender: env.EMAIL_SENDER || 's'
};
}
6 changes: 6 additions & 0 deletions server/src/core/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { createDbConnectionFailedError } from './error';
import { DonationDistributions } from './distribution';
import { SystemLocks } from './system-lock';
import { AtSmsProvider } from './sms';
import { SendGridEmailProvider } from './email';
import { Invitations } from './invitation';
import { EventBus } from './event';
import { UserNotifications } from './user-notification';
Expand Down Expand Up @@ -61,10 +62,15 @@ export async function bootstrap(config: AppConfig): Promise<App> {
apiKey: config.atApiKey,
sender: config.atSmsSender
});
const emailProvider = new SendGridEmailProvider({
apiKey: config.sendgridApiKey,
emailSender: config.emailSender
});

// starts listening to events when instantiated
new UserNotifications({
smsProvider,
emailProvider,
eventBus,
users,
webappBaseUrl: config.webappBaseUrl
Expand Down
2 changes: 2 additions & 0 deletions server/src/core/email/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './types';
export { SendGridEmailProvider } from './sendgrid-email-provider';
38 changes: 38 additions & 0 deletions server/src/core/email/sendgrid-email-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { EmailProvider } from './types';
import { rethrowIfAppError, createEmailDeliveryFailedError, createSendGridApiError } from '../error';
import sgMail = require('@sendgrid/mail');

export interface SendGridEmailProviderArgs {
apiKey: string,
emailSender: string
};

export class SendGridEmailProvider implements EmailProvider{
private emailSender: string;

constructor(args: SendGridEmailProviderArgs) {
sgMail.setApiKey(args.apiKey);
this.emailSender = args.emailSender;
}

async sendEmail(to: string, message: string): Promise<void> {
try {
const res = await sgMail.send({
to,
from: this.emailSender,
subject: 'Social Relief Notification',
text: message,
});

if (res[0].statusCode !== 202) {
throw createEmailDeliveryFailedError('Failed to send email');
}
}
catch (e) {
console.log(e);
rethrowIfAppError(e);
throw createSendGridApiError(e.message);
}
}
}

3 changes: 3 additions & 0 deletions server/src/core/email/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface EmailProvider {
sendEmail(to: string, message: string): Promise<void>;
}
10 changes: 10 additions & 0 deletions server/src/core/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type ErrorCode =
| 'b2cRequestFailed'
| 'serverError'
| 'atApiError'
| 'sendgridApiError'
| 'manualPayApiError'
| 'flutterwaveApiError'
| 'serverError'
Expand All @@ -64,6 +65,7 @@ export type ErrorCode =
| 'systemLockLocked'
| 'systemLockInvalidState'
| 'messageDeliveryFailed'
| 'emailDeliveryFailed'
/**
* This error should only be thrown when a transaction fails
* because the user's transactions are blocked (based on the transactionsBlockedReason field)
Expand Down Expand Up @@ -107,6 +109,10 @@ export function createAtApiError(message: string = messages.ERROR_AT_API_ERROR)
return createAppError(message, 'atApiError');
}

export function createSendGridApiError(message: string) {
return createAppError(message, 'sendgridApiError');
}

export function createManualPayApiError(message: string) {
return createAppError(message, 'manualPayApiError');
}
Expand Down Expand Up @@ -147,6 +153,10 @@ export function createMessageDeliveryFailedError(message: string) {
return createAppError(message, 'messageDeliveryFailed');
}

export function createEmailDeliveryFailedError(message: string) {
return createAppError(message, 'emailDeliveryFailed');
}

/**
* This error should only be thrown when a transaction fails
* because the user's transactions are blocked (based on the transactionsBlockedReason field)
Expand Down
18 changes: 18 additions & 0 deletions server/src/core/user-notification/user-notification.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { SmsProvider } from '../sms';
import { EmailProvider } from '../email';
import { EventBus, Event} from '../event';
import { UserService, UserInvitationEventData } from '../user';
import { TransactionCompletedEventData, Transaction } from '../payment';

export interface UserNotificationsArgs {
smsProvider: SmsProvider;
emailProvider: EmailProvider;
eventBus: EventBus;
users: UserService;
webappBaseUrl: string;
}

export class UserNotifications {
smsProvider: SmsProvider;
emailProvider: EmailProvider;
eventBus: EventBus;
users: UserService;
webappBaseUrl: string;

constructor(args: UserNotificationsArgs) {
this.smsProvider = args.smsProvider;
this.emailProvider = args.emailProvider;
this.eventBus = args.eventBus;
this.users = args.users;
this.webappBaseUrl = args.webappBaseUrl;
Expand All @@ -37,6 +41,9 @@ export class UserNotifications {

try {
await this.smsProvider.sendSms(data.recipientPhone, message);
if (data.recipientEmail) {
await this.emailProvider.sendEmail(data.recipientEmail, message);
}
}
catch (error) {
console.error('Error occurred handling event', event, error);
Expand Down Expand Up @@ -72,11 +79,22 @@ export class UserNotifications {
this.smsProvider.sendSms(donor.phone, donorMessage),
this.smsProvider.sendSms(beneficiary.phone, beneficiaryMessage)
]);

if (donor.email) {
await this.emailProvider.sendEmail(donor.email, donorMessage);
}

if (beneficiary.email) {
await this.emailProvider.sendEmail(beneficiary.email, beneficiaryMessage);
}
}

async sendSuccessfulRefundMessage(transaction: Transaction) {
const user = await this.users.getById(transaction.to);
const message = `Hello ${user.name}, your refund of Ksh ${transaction.amount} from SocialRelief has been issued.`;
await this.smsProvider.sendSms(user.phone, message);
if (user.email) {
await this.emailProvider.sendEmail(user.email, message);
}
}
}
1 change: 1 addition & 0 deletions server/src/core/user/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface UserInvitationEventData {
senderName: string,
recipientName: string,
recipientPhone: string,
recipientEmail: string,
role: string,
invitationId: string
}
Expand Down
1 change: 1 addition & 0 deletions server/src/core/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ export class Users implements UserService {
this.eventBus.emitUserInvitationCreated({
recipientName: invitation.inviteeName,
recipientPhone: invitation.inviteePhone,
recipientEmail: invitation.inviteeEmail,
senderName: invitation.invitorName,
role: invitation.inviteeRole,
invitationId: invitation._id
Expand Down
1 change: 1 addition & 0 deletions server/src/rest/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const errorHandler = (): ErrorRequestHandler =>
case 'validationError':
case 'transactionRejected':
case 'insufficientFunds':
case 'emailDeliveryFailed':
return sendErrorResponse(res, statusCodes.STATUS_BAD_REQUEST, error);
case 'messageDeliveryFailed':
return sendErrorResponse(res, statusCodes.INTERNAL_SERVER_ERROR, error);
Expand Down
26 changes: 25 additions & 1 deletion server/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,30 @@
dependencies:
safe-buffer "^5.1.2"

"@sendgrid/client@^7.2.4":
version "7.2.4"
resolved "https://registry.yarnpkg.com/@sendgrid/client/-/client-7.2.4.tgz#8a7aaa4b16c05c514a1ff93b8ac2613ef80813e8"
integrity sha512-zL56B6f/ftJVnJKtJnXA8g/cvRGOyHAbXeAizYVsG+bjqd4hyRVDAKimHW59b2BvFXLLrphc9yGBt7AKOLJ5Tg==
dependencies:
"@sendgrid/helpers" "^7.2.4"
axios "^0.19.2"

"@sendgrid/helpers@^7.2.4":
version "7.2.4"
resolved "https://registry.yarnpkg.com/@sendgrid/helpers/-/helpers-7.2.4.tgz#82544a4b7d9a905ce5e0b4f96fcd9b2a73129337"
integrity sha512-h8bBRXpjQLM0Zl08EGJPxVOPWBaXjEZZCM3IurxWn8RaVFYDGPVZ4pv4ZW9AAmsujntZXuIxSsmB4aQczg/ivw==
dependencies:
chalk "^2.0.1"
deepmerge "^4.2.2"

"@sendgrid/mail@^7.2.4":
version "7.2.4"
resolved "https://registry.yarnpkg.com/@sendgrid/mail/-/mail-7.2.4.tgz#7633a5ff8cacf6e8df1f51ff2e24cae6c72d448c"
integrity sha512-aBq1LJfp/1yFPVso/hG8FvWdWaVlYQJFVhzMQD8SW8H2MVhPN6WEJsnXw/zNGHxPa5uzO8r7j36ugDOh1LMPDA==
dependencies:
"@sendgrid/client" "^7.2.4"
"@sendgrid/helpers" "^7.2.4"

"@sinonjs/commons@^1.7.0":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2"
Expand Down Expand Up @@ -1279,7 +1303,7 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=

chalk@^2.0.0:
chalk@^2.0.0, chalk@^2.0.1:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
Expand Down

0 comments on commit 44ab618

Please sign in to comment.