Skip to content

Commit 5292cad

Browse files
authored
Merge pull request #2177 from Northeastern-Electric-Racing/#2173-slack-noti-design-review-invite
#2173 slack noti when invited to a design review
2 parents 2138dee + 116df22 commit 5292cad

4 files changed

Lines changed: 169 additions & 154 deletions

File tree

src/backend/src/services/change-requests.services.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,19 @@ import {
2121
DeletedException
2222
} from '../utils/errors.utils';
2323
import changeRequestTransformer from '../transformers/change-requests.transformer';
24-
import {
25-
updateBlocking,
26-
sendSlackCRReviewedNotification,
27-
allChangeRequestsReviewed,
28-
sendSlackCRStatusToThread,
29-
addSlackThreadsToChangeRequest,
30-
sendAndGetSlackCRNotifications
31-
} from '../utils/change-requests.utils';
24+
import { updateBlocking, allChangeRequestsReviewed } from '../utils/change-requests.utils';
3225
import { CR_Type, WBS_Element_Status, User, Scope_CR_Why_Type } from '@prisma/client';
3326
import { getUserFullName, getUsersWithSettings } from '../utils/users.utils';
3427
import { throwIfUncheckedDescriptionBullets } from '../utils/description-bullets.utils';
3528
import workPackageQueryArgs from '../prisma-query-args/work-packages.query-args';
3629
import { buildChangeDetail, createChange } from '../utils/changes.utils';
37-
import { sendSlackRequestedReviewNotification } from '../utils/slack.utils';
30+
import {
31+
addSlackThreadsToChangeRequest,
32+
sendAndGetSlackCRNotifications,
33+
sendSlackCRReviewedNotification,
34+
sendSlackCRStatusToThread,
35+
sendSlackRequestedReviewNotification
36+
} from '../utils/slack.utils';
3837

3938
export default class ChangeRequestsService {
4039
/**

src/backend/src/utils/change-requests.utils.ts

Lines changed: 1 addition & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import prisma from '../prisma/prisma';
2-
import { Scope_CR_Why_Type, Team, User, Prisma, Change_Request, Change, WBS_Element } from '@prisma/client';
2+
import { Scope_CR_Why_Type, User, Prisma, Change_Request, Change } from '@prisma/client';
33
import { addWeeksToDate, ChangeRequestReason } from 'shared';
4-
import { reactToMessage, replyToMessageInThread, sendMessage } from '../integrations/slack';
54
import { HttpException, NotFoundException } from './errors.utils';
65
import { ChangeRequestStatus } from 'shared';
76
import changeRequestRelationArgs from '../prisma-query-args/change-requests.query-args';
@@ -22,146 +21,6 @@ export const convertCRScopeWhyType = (whyType: Scope_CR_Why_Type): ChangeRequest
2221
OTHER: ChangeRequestReason.Other
2322
}[whyType]);
2423

25-
/**
26-
* Sends slack notifications to teams for new CRs and returns the messages sent in slack
27-
*
28-
* @param team the teams of the cr to notify
29-
* @param message the message to send to the teams
30-
* @param crId the cr id
31-
* @param budgetImpact the amount of budget requested for the cr
32-
* @returns the channelId and timestamp of the messages sent in slack
33-
*/
34-
export const sendSlackChangeRequestNotification = async (
35-
team: Team,
36-
message: string,
37-
crId: number,
38-
budgetImpact?: number
39-
): Promise<{ channelId: string; ts: string }[]> => {
40-
if (process.env.NODE_ENV !== 'production') return []; // don't send msgs unless in prod
41-
const msgs: { channelId: string; ts: string }[] = [];
42-
const fullMsg = `:tada: New Change Request! :tada: ${message}`;
43-
const fullLink = `https://finishlinebyner.com/cr/${crId}`;
44-
const btnText = `View CR #${crId}`;
45-
const notification = await sendMessage(team.slackId, fullMsg, fullLink, btnText);
46-
if (notification) msgs.push(notification);
47-
48-
if (budgetImpact && budgetImpact > 100) {
49-
const importantNotification = await sendMessage(
50-
process.env.SLACK_EBOARD_CHANNEL!,
51-
`${fullMsg} with $${budgetImpact} requested`,
52-
fullLink,
53-
btnText
54-
);
55-
if (importantNotification) msgs.push(importantNotification);
56-
}
57-
58-
return msgs;
59-
};
60-
61-
export const sendAndGetSlackCRNotifications = async (
62-
teams: Team[],
63-
changeRequest: Change_Request,
64-
submitter: User,
65-
wbsElement: WBS_Element,
66-
projectWbsName: string
67-
) => {
68-
const notifications: { channelId: string; ts: string }[] = [];
69-
let message = '';
70-
switch (changeRequest.type) {
71-
case 'ACTIVATION':
72-
message = `${submitter.firstName} ${submitter.lastName} wants to activate ${wbsElement.name} in ${projectWbsName}`;
73-
break;
74-
case 'STAGE_GATE':
75-
message = `${submitter.firstName} ${submitter.lastName} wants to stage gate ${wbsElement.name} in ${projectWbsName}`;
76-
break;
77-
default:
78-
message = `${changeRequest.type} CR submitted by ${submitter.firstName} ${submitter.lastName} for the ${projectWbsName} project`;
79-
}
80-
81-
const completion: Promise<void>[] = teams.map(async (team) => {
82-
const sentNotifications: { channelId: string; ts: string }[] = await sendSlackChangeRequestNotification(
83-
team,
84-
message,
85-
changeRequest.crId
86-
);
87-
if (sentNotifications) notifications.push(...sentNotifications);
88-
});
89-
await Promise.all(completion);
90-
91-
return notifications;
92-
};
93-
94-
export const sendSlackCRReviewedNotification = async (slackId: string, crId: number) => {
95-
if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
96-
const msgs = [];
97-
const fullMsg = `:tada: Your Change Request was just reviewed! Click the link to view! :tada:`;
98-
const fullLink = `https://finishlinebyner.com/cr/${crId}`;
99-
const btnText = `View CR#${crId}`;
100-
msgs.push(sendMessage(slackId, fullMsg, fullLink, btnText));
101-
102-
return Promise.all(msgs);
103-
};
104-
105-
/**
106-
* Replies and reacts to slack messages with the new change request status
107-
*
108-
* @param threads the threads of cr slack notifications to reply/react to
109-
* @param crId the cr id
110-
* @param approved is the cr approved
111-
*/
112-
export const sendSlackCRStatusToThread = async (
113-
threads: {
114-
messageInfoId: string;
115-
channelId: string;
116-
timestamp: string;
117-
changeRequestId: number;
118-
}[],
119-
crId: number,
120-
approved: boolean
121-
) => {
122-
if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
123-
const fullMsg = `This Change Request was ${approved ? 'approved! :tada:' : 'denied.'} Click the link to view.`;
124-
const fullLink = `https://finishlinebyner.com/cr/${crId}`;
125-
const btnText = `View CR#${crId}`;
126-
try {
127-
if (threads && threads.length !== 0) {
128-
const msgs = threads.map((thread) =>
129-
replyToMessageInThread(thread.channelId, thread.timestamp, fullMsg, fullLink, btnText)
130-
);
131-
const reactions = threads.map((thread) =>
132-
reactToMessage(thread.channelId, thread.timestamp, approved ? 'white_check_mark' : 'x')
133-
);
134-
await Promise.all([...msgs, ...reactions]);
135-
}
136-
} catch (err: unknown) {
137-
if (err instanceof Error) {
138-
throw new HttpException(500, `Failed to send slack notification: ${err.message}`);
139-
}
140-
}
141-
};
142-
143-
/**
144-
* Adds the relevant slack notifications for a change request to the change request
145-
*
146-
* @param crId the change request to add the slack threads to
147-
* @param notifications the slack threads to add to the change request
148-
*/
149-
export const addSlackThreadsToChangeRequest = async (crId: number, threads: { channelId: string; ts: string }[]) => {
150-
const promises = threads.map((notification) =>
151-
prisma.message_Info.create({
152-
data: {
153-
changeRequestId: crId,
154-
channelId: notification.channelId,
155-
timestamp: notification.ts
156-
},
157-
include: {
158-
changeRequest: true
159-
}
160-
})
161-
);
162-
await Promise.all(promises);
163-
};
164-
16524
/**
16625
* This function updates the start date of all the blockings (and nested blockings) of the initial given work package.
16726
* It uses a depth first search algorithm for efficiency and to avoid cycles.

src/backend/src/utils/slack.utils.ts

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { ChangeRequest, daysBetween, Task, User, wbsPipe, WorkPackage } from 'shared';
2-
import { sendMessage } from '../integrations/slack';
2+
import { reactToMessage, replyToMessageInThread, sendMessage } from '../integrations/slack';
33
import { getUserSlackId } from './users.utils';
44
import prisma from '../prisma/prisma';
55
import { HttpException } from './errors.utils';
6+
import { Change_Request, Team, WBS_Element } from '@prisma/client';
67

78
// build the "due" string for the upcoming deadlines slack message
89
const buildDueString = (daysUntilDeadline: number): string => {
@@ -89,3 +90,158 @@ export const sendReimbursementRequestDeniedNotification = async (slackId: string
8990
}
9091
}
9192
};
93+
94+
export const sendSlackDesignReviewNotification = async (slackId: string, designReviewId: string) => {
95+
if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
96+
const msg = `You have been invited to a Design Review!`;
97+
const fullLink = `https://finishlinebyner.com/design-reviews/${designReviewId}`;
98+
const linkButtonText = 'RSVP for the Design Review';
99+
100+
try {
101+
await sendMessage(slackId, msg, fullLink, linkButtonText);
102+
} catch (error: unknown) {
103+
if (error instanceof Error) {
104+
throw new HttpException(500, `Failed to send slack notification: ${error.message}`);
105+
}
106+
}
107+
};
108+
109+
/**
110+
* Sends slack notifications to teams for new CRs and returns the messages sent in slack
111+
*
112+
* @param team the teams of the cr to notify
113+
* @param message the message to send to the teams
114+
* @param crId the cr id
115+
* @param budgetImpact the amount of budget requested for the cr
116+
* @returns the channelId and timestamp of the messages sent in slack
117+
*/
118+
export const sendSlackChangeRequestNotification = async (
119+
team: Team,
120+
message: string,
121+
crId: number,
122+
budgetImpact?: number
123+
): Promise<{ channelId: string; ts: string }[]> => {
124+
if (process.env.NODE_ENV !== 'production') return []; // don't send msgs unless in prod
125+
const msgs: { channelId: string; ts: string }[] = [];
126+
const fullMsg = `:tada: New Change Request! :tada: ${message}`;
127+
const fullLink = `https://finishlinebyner.com/cr/${crId}`;
128+
const btnText = `View CR #${crId}`;
129+
const notification = await sendMessage(team.slackId, fullMsg, fullLink, btnText);
130+
if (notification) msgs.push(notification);
131+
132+
if (budgetImpact && budgetImpact > 100) {
133+
const importantNotification = await sendMessage(
134+
process.env.SLACK_EBOARD_CHANNEL!,
135+
`${fullMsg} with $${budgetImpact} requested`,
136+
fullLink,
137+
btnText
138+
);
139+
if (importantNotification) msgs.push(importantNotification);
140+
}
141+
142+
return msgs;
143+
};
144+
145+
export const sendAndGetSlackCRNotifications = async (
146+
teams: Team[],
147+
changeRequest: Change_Request,
148+
submitter: User,
149+
wbsElement: WBS_Element,
150+
projectWbsName: string
151+
) => {
152+
const notifications: { channelId: string; ts: string }[] = [];
153+
let message = '';
154+
switch (changeRequest.type) {
155+
case 'ACTIVATION':
156+
message = `${submitter.firstName} ${submitter.lastName} wants to activate ${wbsElement.name} in ${projectWbsName}`;
157+
break;
158+
case 'STAGE_GATE':
159+
message = `${submitter.firstName} ${submitter.lastName} wants to stage gate ${wbsElement.name} in ${projectWbsName}`;
160+
break;
161+
default:
162+
message = `${changeRequest.type} CR submitted by ${submitter.firstName} ${submitter.lastName} for the ${projectWbsName} project`;
163+
}
164+
165+
const completion: Promise<void>[] = teams.map(async (team) => {
166+
const sentNotifications: { channelId: string; ts: string }[] = await sendSlackChangeRequestNotification(
167+
team,
168+
message,
169+
changeRequest.crId
170+
);
171+
if (sentNotifications) notifications.push(...sentNotifications);
172+
});
173+
await Promise.all(completion);
174+
175+
return notifications;
176+
};
177+
178+
export const sendSlackCRReviewedNotification = async (slackId: string, crId: number) => {
179+
if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
180+
const msgs = [];
181+
const fullMsg = `:tada: Your Change Request was just reviewed! Click the link to view! :tada:`;
182+
const fullLink = `https://finishlinebyner.com/cr/${crId}`;
183+
const btnText = `View CR#${crId}`;
184+
msgs.push(sendMessage(slackId, fullMsg, fullLink, btnText));
185+
186+
return Promise.all(msgs);
187+
};
188+
189+
/**
190+
* Replies and reacts to slack messages with the new change request status
191+
*
192+
* @param threads the threads of cr slack notifications to reply/react to
193+
* @param crId the cr id
194+
* @param approved is the cr approved
195+
*/
196+
export const sendSlackCRStatusToThread = async (
197+
threads: {
198+
messageInfoId: string;
199+
channelId: string;
200+
timestamp: string;
201+
changeRequestId: number;
202+
}[],
203+
crId: number,
204+
approved: boolean
205+
) => {
206+
if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
207+
const fullMsg = `This Change Request was ${approved ? 'approved! :tada:' : 'denied.'} Click the link to view.`;
208+
const fullLink = `https://finishlinebyner.com/cr/${crId}`;
209+
const btnText = `View CR#${crId}`;
210+
try {
211+
if (threads && threads.length !== 0) {
212+
const msgs = threads.map((thread) =>
213+
replyToMessageInThread(thread.channelId, thread.timestamp, fullMsg, fullLink, btnText)
214+
);
215+
const reactions = threads.map((thread) =>
216+
reactToMessage(thread.channelId, thread.timestamp, approved ? 'white_check_mark' : 'x')
217+
);
218+
await Promise.all([...msgs, ...reactions]);
219+
}
220+
} catch (err: unknown) {
221+
if (err instanceof Error) {
222+
throw new HttpException(500, `Failed to send slack notification: ${err.message}`);
223+
}
224+
}
225+
};
226+
227+
/**
228+
* Adds the relevant slack notifications for a change request to the change request
229+
*
230+
* @param crId the change request to add the slack threads to
231+
* @param notifications the slack threads to add to the change request
232+
*/
233+
export const addSlackThreadsToChangeRequest = async (crId: number, threads: { channelId: string; ts: string }[]) => {
234+
const promises = threads.map((notification) =>
235+
prisma.message_Info.create({
236+
data: {
237+
changeRequestId: crId,
238+
channelId: notification.channelId,
239+
timestamp: notification.ts
240+
},
241+
include: {
242+
changeRequest: true
243+
}
244+
})
245+
);
246+
await Promise.all(promises);
247+
};

src/backend/tests/change-requests.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,15 @@ import {
3333
} from '../src/utils/errors.utils';
3434
import * as changeRequestTransformer from '../src/transformers/change-requests.transformer';
3535
import * as changeRequestUtils from '../src/utils/change-requests.utils';
36+
import * as slackUtils from '../src/utils/slack.utils';
3637

3738
describe('Change Requests', () => {
3839
beforeEach(() => {
3940
vi.spyOn(changeRequestTransformer, 'default').mockReturnValue(sharedChangeRequest);
40-
vi.spyOn(changeRequestUtils, 'sendSlackCRReviewedNotification').mockImplementation(async (_slackId, _crId) => {
41+
vi.spyOn(slackUtils, 'sendSlackCRReviewedNotification').mockImplementation(async (_slackId, _crId) => {
4142
return [];
4243
});
43-
vi.spyOn(changeRequestUtils, 'sendSlackChangeRequestNotification').mockImplementation(async (_slackId, _crId) => {
44+
vi.spyOn(slackUtils, 'sendSlackChangeRequestNotification').mockImplementation(async (_slackId, _crId) => {
4445
return [];
4546
});
4647
vi.spyOn(changeRequestUtils, 'updateBlocking').mockImplementation(async () => {});

0 commit comments

Comments
 (0)