Skip to content

Commit

Permalink
emails and notifications on loan application status change (#814)
Browse files Browse the repository at this point in the history
  • Loading branch information
Bernardo Vieira authored Aug 15, 2023
1 parent 81ffe15 commit 07474ab
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 123 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/routes/v2/microcredit/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export default (route: Router): void => {
* tags:
* - "microcredit"
* summary: "Update microcredit applications"
* description: "Status can be 0: pending, 1: submitted, 2: in-review, 3: requested-changes, 4: approved, 5: rejected"
* description: "Status can be 0: pending, 1: submitted, 2: in-review, 3: requested-changes, 4: interview, 5: approved, 6: rejected"
* requestBody:
* content:
* application/json:
Expand Down
34 changes: 25 additions & 9 deletions packages/core/src/interfaces/app/appNotification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,31 @@
*/

export enum NotificationType {
STORY_LIKED,
BENEFICIARY_ADDED,
MANAGER_ADDED,
COMMUNITY_CREATED,
LOAN_ADDED,
LEARN_AND_EARN_DO_NEW_LESSON,
LOAN_STATUS_CHANGED,
LOAN_APPLICATION_SUBMITTED,
NEW_LOAN_SUBMITTED
STORY_LIKED = 0,
BENEFICIARY_ADDED = 1,
MANAGER_ADDED = 2,
COMMUNITY_CREATED = 3,
// LOAN_ADDED = 4, // removed
LEARN_AND_EARN_DO_NEW_LESSON = 5,
// LOAN_STATUS_CHANGED = 6, // removed
LOAN_APPLICATION_SUBMITTED = 7,
LOAN_APPLICATION_RECEIVED = 8,
LOAN_APPLICATION_APPROVED = 9,
LOAN_APPLICATION_REJECTED = 10,
LOAN_APPLICATION_REQUEST_CHANGES = 11,
LOAN_APPLICATION_INTERVIEW = 12,
TRANSACTION_RECEIVED = 13
}

export type NotificationParams = {
path?: string;
};

export enum NotificationParamsPath {
STORY = 'stories?id=',
COMMUNITY = 'communities/',
LOAN_APPLICATION = 'microcredit/form/',
LOAN_APPROVED = 'microcredit/'
}

export interface AppNotification {
Expand Down
215 changes: 144 additions & 71 deletions packages/core/src/services/microcredit/create.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as prismicH from '@prismicio/helpers';
import { AppUser } from '../../interfaces/app/appUser';
import { AppUserModel } from '../../database/models/app/appUser';
import { BaseError, prismic as basePrimisc, locales } from '../../utils';
import { MailDataRequired } from '@sendgrid/mail';
Expand All @@ -8,7 +9,7 @@ import {
MicroCreditApplicationStatus
} from '../../interfaces/microCredit/applications';
import { MicroCreditContentStorage } from '../../services/storage';
import { NotificationType } from '../../interfaces/app/appNotification';
import { NotificationParamsPath, NotificationType } from '../../interfaces/app/appNotification';
import { NullishPropertiesOf } from 'sequelize/types/utils';
import { Op, Optional, Transaction, WhereOptions } from 'sequelize';
import { config } from '../../..';
Expand Down Expand Up @@ -150,7 +151,7 @@ export default class MicroCreditCreate {
{
model: models.appUser,
as: 'user',
attributes: ['id', 'walletPNT', 'appPNT', 'language']
attributes: ['id', 'email', 'walletPNT', 'appPNT', 'language']
}
]
});
Expand All @@ -160,7 +161,31 @@ export default class MicroCreditCreate {
.map(application => application.user)
.filter(user => user !== undefined) as AppUserModel[];

await sendNotification(users, NotificationType.LOAN_STATUS_CHANGED, true, true, undefined, transaction);
// we are assuming that all applications have the same status
let notificationType: NotificationType = NotificationType.LOAN_APPLICATION_INTERVIEW;
switch (status[0]) {
case MicroCreditApplicationStatus.APPROVED:
notificationType = NotificationType.LOAN_APPLICATION_APPROVED;
break;
case MicroCreditApplicationStatus.REJECTED:
notificationType = NotificationType.LOAN_APPLICATION_REJECTED;
break;
case MicroCreditApplicationStatus.REQUEST_CHANGES:
notificationType = NotificationType.LOAN_APPLICATION_REQUEST_CHANGES;
break;
}

users.forEach((user, i) => {
this._notifyApplicationChangeStatusByEmail(user, applicationIds[i], notificationType);
});
await sendNotification(
users,
notificationType,
true,
true,
applicationIds.map(a => ({ path: `${NotificationParamsPath.LOAN_APPLICATION}${a}` })),
transaction
);
}

public saveForm = async (
Expand Down Expand Up @@ -203,10 +228,12 @@ export default class MicroCreditCreate {
id: userForm.selectedLoanManagerId
}
})
.then(user => user && sendNotification([user], NotificationType.NEW_LOAN_SUBMITTED, true, true));
.then(
user =>
user && sendNotification([user], NotificationType.LOAN_APPLICATION_RECEIVED, false, true)
);
// below we will send email to user with form
// get user email and language to get text for template and send email
const urlToApplicationForm = `https://app.impactmarket.com/microcredit/form/${userForm.id}`;
const user = await models.appUser.findOne({
attributes: ['email', 'language', 'walletPNT', 'appPNT'],
where: {
Expand All @@ -217,73 +244,13 @@ export default class MicroCreditCreate {
throw new BaseError('USER_NOT_FOUND', 'User not found');
}

// get text for email template on user language defaulting to english
const locale = locales.find(({ shortCode }) => user?.language.toLowerCase() === shortCode.toLowerCase())
?.code;
const response = await prismic.getAllByType('push_notifications_data', {
lang: locale || 'en-US'
});
let submittedFormEmailNotificationSubject: string | undefined;
let submittedFormEmailNotificationTitle: string | undefined;
let submittedFormEmailNotificationSubtitle: string | undefined;
let submittedFormEmailNotificationViewApplication: string | undefined;
let submittedFormEmailNotificationViewNextSteps: string | undefined;
if (response.length > 0) {
const data = response[0].data;
submittedFormEmailNotificationSubject = data['submitted-form-email-notification-subject'];
submittedFormEmailNotificationTitle = data['submitted-form-email-notification-title'];
submittedFormEmailNotificationSubtitle = data['submitted-form-email-notification-subtitle'];
submittedFormEmailNotificationViewApplication =
data['submitted-form-email-notification-view-application'];
submittedFormEmailNotificationViewNextSteps =
prismicH.asHTML(data['submitted-form-email-notification-next-steps']) || '';
}
if (
!submittedFormEmailNotificationSubject ||
!submittedFormEmailNotificationTitle ||
!submittedFormEmailNotificationSubtitle ||
!submittedFormEmailNotificationViewApplication ||
!submittedFormEmailNotificationViewNextSteps
) {
const response = await prismic.getAllByType('push_notifications_data', {
lang: 'en-US'
});
const data = response[0].data;
submittedFormEmailNotificationSubject = data['submitted-form-email-notification-subject'];
submittedFormEmailNotificationTitle = data['submitted-form-email-notification-title'];
submittedFormEmailNotificationSubtitle = data['submitted-form-email-notification-subtitle'];
submittedFormEmailNotificationViewApplication =
data['submitted-form-email-notification-view-application'];
submittedFormEmailNotificationViewNextSteps =
prismicH.asHTML(data['submitted-form-email-notification-next-steps']) || '';
}

// build the email structure and send
const dynamicTemplateData = {
submittedFormEmailNotificationTitle,
submittedFormEmailNotificationSubtitle,
urlToApplicationForm,
submittedFormEmailNotificationViewApplication,
submittedFormEmailNotificationViewNextSteps,
submittedFormEmailNotificationSubject
};
const personalizations = [
{
to: [{ email: user.email }],
dynamicTemplateData
}
];
const sendgridData: MailDataRequired = {
from: {
name: 'impactMarket',
email: 'hello@impactmarket.com'
},
personalizations,
templateId: 'd-b257690897ff41028d7ad8cabe88f8cb'
};
sendEmail(sendgridData);
this._notifyApplicationChangeStatusByEmail(
user,
userForm.id,
NotificationType.LOAN_APPLICATION_SUBMITTED
);
sendNotification([user.toJSON()], NotificationType.LOAN_APPLICATION_SUBMITTED, true, true, {
url: urlToApplicationForm
path: `${NotificationParamsPath.LOAN_APPLICATION}${userForm.id}`
});
}

Expand Down Expand Up @@ -319,4 +286,110 @@ export default class MicroCreditCreate {
note
});
};

private async _notifyApplicationChangeStatusByEmail(
user: AppUser,
formId: number,
notificationType: NotificationType
) {
let baseKey = 'submitted';
let emailType = MicroCreditApplicationStatus.PENDING;
let urlPath = NotificationParamsPath.LOAN_APPLICATION;
switch (notificationType) {
// case NotificationType.LOAN_APPLICATION_SUBMITTED:
case NotificationType.LOAN_APPLICATION_REQUEST_CHANGES:
baseKey = 'requested-changes';
emailType = MicroCreditApplicationStatus.REQUEST_CHANGES;
break;
case NotificationType.LOAN_APPLICATION_APPROVED:
baseKey = 'approved';
emailType = MicroCreditApplicationStatus.APPROVED;
urlPath = NotificationParamsPath.LOAN_APPROVED;
break;
case NotificationType.LOAN_APPLICATION_INTERVIEW:
baseKey = 'interview';
emailType = MicroCreditApplicationStatus.INTERVIEW;
urlPath = NotificationParamsPath.LOAN_APPROVED;
break;
case NotificationType.LOAN_APPLICATION_REJECTED:
baseKey = 'rejected';
emailType = MicroCreditApplicationStatus.REJECTED;
break;
}
let buttonURL = `https://app.impactmarket.com/${urlPath}${formId}`;
if (urlPath === NotificationParamsPath.LOAN_APPROVED) {
buttonURL = `https://app.impactmarket.com/${urlPath}`;
}
// get text for email template on user language defaulting to english
const locale = locales.find(({ shortCode }) => user.language.toLowerCase() === shortCode.toLowerCase())?.code;
const response = await prismic.getAllByType('push_notifications_data', {
lang: locale || 'en-US'
});
let subject: string | undefined;
let title: string | undefined;
let subtitle: string | undefined;
let buttonText: string | undefined;
let body: string | undefined;
if (response.length > 0) {
const data = response[0].data;
subject = data[`${baseKey}-form-email-notification-subject`];
title = data[`${baseKey}-form-email-notification-title`];
subtitle = data[`${baseKey}-form-email-notification-subtitle`];
buttonText = data[`${baseKey}-form-email-notification-button-text`];
body = data[`${baseKey}-form-email-notification-body`]
? prismicH.asHTML(data[`${baseKey}-form-email-notification-body`]) || undefined
: undefined;
}
if (!subject || !title || !subtitle || !buttonText || !body) {
const response = await prismic.getAllByType('push_notifications_data', {
lang: 'en-US'
});
const data = response[0].data;
subject = data[`${baseKey}-form-email-notification-subject`];
title = data[`${baseKey}-form-email-notification-title`];
subtitle = data[`${baseKey}-form-email-notification-subtitle`];
buttonText = data[`${baseKey}-form-email-notification-button-text`];
body = data[`${baseKey}-form-email-notification-body`]
? prismicH.asHTML(data[`${baseKey}-form-email-notification-body`]) || undefined
: undefined;
}

if (notificationType === NotificationType.LOAN_APPLICATION_APPROVED) {
const formData = await models.microCreditApplications.findOne({
attributes: ['amount'],
where: {
id: formId
}
});
subtitle = subtitle!
.replace('{{amount}}', formData!.amount?.toString() || '0')
.replace('{{currency}}', 'cUSD');
}

// build the email structure and send
const dynamicTemplateData = {
title,
subtitle,
buttonURL,
buttonText,
body,
subject,
emailType
};
const personalizations = [
{
to: [{ email: user.email }],
dynamicTemplateData
}
];
const sendgridData: MailDataRequired = {
from: {
name: 'impactMarket',
email: 'hello@impactmarket.com'
},
personalizations,
templateId: 'd-b257690897ff41028d7ad8cabe88f8cb'
};
sendEmail(sendgridData);
}
}
25 changes: 1 addition & 24 deletions packages/core/src/services/story/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,6 @@ export default class StoryServiceV2 {
where: { contentId, address: userAddress }
});
} else {
this.addNotification(userAddress, contentId);
const user = await models.appUser.findOne({
attributes: ['id', 'language', 'walletPNT', 'appPNT'],
where: {
Expand All @@ -527,7 +526,7 @@ export default class StoryServiceV2 {
});

if (user) {
await sendNotification([user.toJSON()], NotificationType.STORY_LIKED);
await sendNotification([user.toJSON()], NotificationType.STORY_LIKED, false);
}

await models.storyUserEngagement.create({
Expand Down Expand Up @@ -660,26 +659,4 @@ export default class StoryServiceV2 {
throw new BaseError('ERROR', 'comment was not deleted');
}
}

private async addNotification(userAddress: string, contentId: number) {
const story = (await models.storyContent.findOne({
attributes: [],
where: { id: contentId },
include: [
{
model: models.appUser,
as: 'user',
attributes: ['id']
}
]
}))! as StoryContent;

await models.appNotification.findOrCreate({
where: {
userId: story.user!.id,
type: NotificationType.STORY_LIKED,
params: { userAddress, contentId }
}
});
}
}
14 changes: 3 additions & 11 deletions packages/core/src/subscriber/chainSubscribers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Logger } from '../utils/logger';
import { MicroCreditApplicationStatus } from '../interfaces/microCredit/applications';
import { Create as MicroCreditCreate } from '../services/microcredit';
import { NotificationType } from '../interfaces/app/appNotification';
import { NotificationParamsPath, NotificationType } from '../interfaces/app/appNotification';
import { Transaction } from 'sequelize';
import { config, contracts, database, services, utils } from '../../';
import { ethers } from 'ethers';
Expand Down Expand Up @@ -248,7 +248,7 @@ class ChainSubscribers {
true,
true,
{
communityId: affectedRows[0].id
path: `${NotificationParamsPath.COMMUNITY}${affectedRows[0].id}`
},
transaction
);
Expand Down Expand Up @@ -287,7 +287,7 @@ class ChainSubscribers {
true,
true,
{
communityId: community
path: `${NotificationParamsPath.COMMUNITY}${community}`
},
transaction
);
Expand Down Expand Up @@ -341,14 +341,6 @@ class ChainSubscribers {
[userAddress],
[MicroCreditApplicationStatus.APPROVED],
transaction
),
sendNotification(
[user.toJSON()],
NotificationType.LOAN_ADDED,
true,
true,
undefined,
transaction
)
]);
if (!created) {
Expand Down
Loading

0 comments on commit 07474ab

Please sign in to comment.