Skip to content

Commit

Permalink
revised email notification files to use event subscriber model; chang…
Browse files Browse the repository at this point in the history
…ed design to pull emails for byPass and transitions from the config file
  • Loading branch information
atGit2021 committed Mar 23, 2023
1 parent 4a1baff commit 6663fa6
Show file tree
Hide file tree
Showing 10 changed files with 386 additions and 258 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ID, Session, UnsecuredDto } from '~/common';
import { ProgressReportStatus as Status } from '../../dto';
import { ProgressReportWorkflowEvent as WorkflowEvent } from '../dto/workflow-event.dto';
import { InternalTransition } from '../transitions';

export class WorkflowUpdatedEvent {
constructor(
readonly reportId: ID,
readonly previousStatus: Status,
readonly next: InternalTransition | Status,
readonly session: Session,
readonly workflowEvent: UnsecuredDto<WorkflowEvent>,
) {}
}
1 change: 1 addition & 0 deletions src/components/progress-report/workflow/handlers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './progress-report-workflow-notification.handler';
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { EmailService } from '@seedcompany/nestjs-email';
import { uniqBy } from 'lodash';
import { ID, Role, Session, UnsecuredDto } from '~/common';
import {
ConfigService,
EventsHandler,
IEventHandler,
ILogger,
Logger,
} from '~/core';
import {
EmailReportStatusNotification,
ProgressReportStatusChanged,
} from '~/core/email/templates/progress-report-status-changed.template';
import { AuthenticationService } from '../../../authentication';
import { LanguageService } from '../../../language';
import { PeriodicReportService } from '../../../periodic-report';
import { ProjectService } from '../../../project';
import { UserService } from '../../../user';
import { ProgressReportStatus as Status } from '../../dto';
import { ProgressReportWorkflowEvent } from '../dto/workflow-event.dto';
import { WorkflowUpdatedEvent } from '../events/workflow-updated.event';
import { ProgressReportWorkflowRepository } from '../progress-report-workflow.repository';
import { ProgressReportWorkflowService } from '../progress-report-workflow.service';
import { InternalTransition } from '../transitions';

interface Member {
id?: ID | undefined;
email?: string | undefined;
role?: Role[] | undefined;
}
type Recipient = Omit<Member, 'role'>;

@EventsHandler(WorkflowUpdatedEvent)
export class ProgressReportWorkflowNotificationHandler
implements IEventHandler<WorkflowUpdatedEvent>
{
constructor(
private readonly auth: AuthenticationService,
private readonly repo: ProgressReportWorkflowRepository,
private readonly configService: ConfigService,
private readonly userService: UserService,
private readonly projectService: ProjectService,
private readonly languageService: LanguageService,
private readonly reportService: PeriodicReportService,
private readonly emailService: EmailService,
private readonly workflowService: ProgressReportWorkflowService,
@Logger('ProgressReportWorkflowNotificationHandler:report-status-changed')
private readonly logger: ILogger,
) {}

async handle({
reportId,
previousStatus,
next,
session,
workflowEvent,
}: WorkflowUpdatedEvent) {
if (!this.configService.sendProgressReportNotifications) {
return;
}

const membersWithValidRoles = await this.getTeamMembersWithValidRoles(
reportId,
next,
);

const envEmailList = this.getEnvList(next) as [];
const usersInEnvList: Recipient[] = await Promise.all(
envEmailList.map(async (item) => {
const user = await this.repo.getUserInfoByEmail(item);
if (user.length > 0) {
const [{ id, email }] = user;
return { id: id, email: email };
} else {
return { id: undefined, email: item };
}
}),
);

const recipients = uniqBy(
[...usersInEnvList, ...membersWithValidRoles],
'email',
);
const notifications: EmailReportStatusNotification[] = await Promise.all(
recipients.map((recipient) =>
this.prepareNotificationObject(
session,
reportId,
previousStatus,
recipient,
workflowEvent,
),
),
);

this.logger.info('Notifying', {
emails: notifications.map((r) => r.recipient.email.value),
projectId: notifications[0]?.project.id ?? undefined,
languageId: notifications[0]?.language.id ?? undefined,
reportId: reportId,
newStatusVal: notifications[0]?.newStatusVal ?? undefined,
previousStatusVal: notifications[0]?.previousStatusVal ?? undefined,
});

for (const notification of notifications) {
if (!notification.recipient.email.value) {
continue;
}
await this.emailService.send(
notification.recipient.email.value,
ProgressReportStatusChanged,
notification,
);
}
}

private async prepareNotificationObject(
session: Session,
reportId: ID,
previousStatus: Status,
receiver: Recipient,
unsecuredEvent: UnsecuredDto<ProgressReportWorkflowEvent>,
): Promise<EmailReportStatusNotification> {
const { projectId, languageId } = await this.repo.getProjectInfoByReportId(
reportId,
);

const recipientId = receiver.id
? receiver.id
: this.configService.rootAdmin.id;
const recipientSession = await this.auth.sessionForUser(recipientId);

const recipient = receiver.id
? await this.userService.readOne(recipientId, recipientSession)
: {
email: { value: receiver.email, canRead: true, canEdit: false },
displayFirstName: {
value: receiver.email?.split('@')[0],
canRead: true,
canEdit: false,
},
displayLastName: { value: '', canRead: true, canEdit: false },
timezone: {
value: this.configService.defaultTimeZone,
canRead: true,
canEdit: false,
},
};

const project = await this.projectService.readOne(
projectId,
recipientSession,
);
const language = await this.languageService.readOne(
languageId,
recipientSession,
);
const report = await this.reportService.readOne(reportId, recipientSession);
const changedBy = await this.userService.readOne(
session.userId,
recipientSession,
);
const workflowEvent = this.workflowService.secure(
unsecuredEvent,
recipientSession,
);

return {
changedBy,
recipient,
project,
language,
report,
newStatusVal: report.status?.value,
previousStatusVal: previousStatus,
workflowEvent: workflowEvent,
};
}

private async getTeamMembersWithValidRoles(
reportId: ID,
next: Status | InternalTransition,
) {
const rolesToAlwaysNotify = [
Role.ProjectManager,
Role.RegionalDirector,
Role.FieldOperationsDirector,
];
const allRolesToNotify =
typeof next !== 'string' && next.notify.TeamMembersWithRoles
? [...rolesToAlwaysNotify, ...next.notify.TeamMembersWithRoles]
: [...rolesToAlwaysNotify];

const projectMemberInfo = (await this.repo.getProjectMemberInfoByReportId(
reportId,
)) as Member[];
return projectMemberInfo
.filter((mbr) =>
mbr.role?.some((role) => allRolesToNotify.includes(role)),
)
.map((mbr) => {
delete mbr.role;
return mbr;
});
}

private getEnvList(next: Status | InternalTransition) {
let envList: string | string[] = [];
typeof next !== 'string'
? this.parseEnv(this.configService.progressReportTransitionEmails)
.filter((item) => item[0] === next.name)
.map((item) => (envList = item[1]))
: this.parseEnv(this.configService.progressReportBypassEmails)
.filter((item) => item[0] === next)
.map((item) => (envList = item[1]));
return envList;
}

private parseEnv(envString: string) {
return envString
?.split(';')
.map((item) => item.split('='))
?.map((item) => {
return [item[0], item[1].split(',')];
});
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { LanguageModule } from '../../language/language.module';
import { PeriodicReportModule } from '../../periodic-report/periodic-report.module';
import { ProjectModule } from '../../project/project.module';
import { UserModule } from '../../user/user.module';
import * as handlers from './handlers';
import { ProgressReportWorkflowEventLoader } from './progress-report-workflow-event.loader';
import { ProgressReportWorkflowFlowchart } from './progress-report-workflow.flowchart';
import { ProgressReportWorkflowEventGranter } from './progress-report-workflow.granter';
Expand All @@ -12,7 +15,12 @@ import { ProgressReportWorkflowEventResolver } from './resolvers/progress-report
import { ProgressReportWorkflowEventsResolver } from './resolvers/progress-report-workflow-events.resolver';

@Module({
imports: [UserModule, ProjectModule],
imports: [
UserModule,
ProjectModule,
LanguageModule,
forwardRef(() => PeriodicReportModule),
],
providers: [
ProgressReportTransitionsResolver,
ProgressReportExecuteTransitionResolver,
Expand All @@ -23,6 +31,7 @@ import { ProgressReportWorkflowEventsResolver } from './resolvers/progress-repor
ProgressReportWorkflowEventGranter,
ProgressReportWorkflowRepository,
ProgressReportWorkflowFlowchart,
...Object.values(handlers),
],
})
export class ProgressReportWorkflowModule {}
Loading

0 comments on commit 6663fa6

Please sign in to comment.