Skip to content

Commit f2c231c

Browse files
committed
#1592 Move logic to the backend
1 parent 4677a0d commit f2c231c

10 files changed

Lines changed: 140 additions & 103 deletions

File tree

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,19 @@ export default class ChangeRequestsController {
7575

7676
static async createStandardChangeRequest(req: Request, res: Response, next: NextFunction) {
7777
try {
78-
const { wbsNum, type, what, why } = req.body;
78+
const { wbsNum, type, what, why, proposedSolutions } = req.body;
7979
const submitter = await getCurrentUser(res);
80-
const id = await ChangeRequestsService.createStandardChangeRequest(
80+
const createdCR = await ChangeRequestsService.createStandardChangeRequest(
8181
submitter,
8282
wbsNum.carNumber,
8383
wbsNum.projectNumber,
8484
wbsNum.workPackageNumber,
8585
type,
8686
what,
87-
why
87+
why,
88+
proposedSolutions
8889
);
89-
return res.status(200).json({ message: `${id}` });
90+
return res.status(200).json(createdCR);
9091
} catch (error: unknown) {
9192
next(error);
9293
}

src/backend/src/integrations/slack.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { WebClient } from '@slack/web-api';
2+
import { HttpException } from '../utils/errors.utils';
23

34
const slack = new WebClient(process.env.SLACK_BOT_TOKEN);
45

@@ -40,13 +41,17 @@ export const sendMessage = async (slackId: string, message: string, link?: strin
4041
}
4142
};
4243

43-
await slack.chat.postMessage({
44-
token: SLACK_BOT_TOKEN,
45-
channel: slackId,
46-
text: message,
47-
blocks: [block],
48-
unfurl_links: false
49-
});
44+
try {
45+
await slack.chat.postMessage({
46+
token: SLACK_BOT_TOKEN,
47+
channel: slackId,
48+
text: message,
49+
blocks: [block],
50+
unfurl_links: false
51+
});
52+
} catch (error) {
53+
throw new HttpException(500, 'Error sending slack message, reason: ' + (error as any).data.error);
54+
}
5055
};
5156

5257
export default slack;

src/backend/src/prisma/seed.ts

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import ChangeRequestsService from '../services/change-requests.services';
2222
import projectQueryArgs from '../prisma-query-args/projects.query-args';
2323
import TeamsService from '../services/teams.services';
2424
import WorkPackagesService from '../services/work-packages.services';
25-
import { ClubAccount, validateWBS, WbsElementStatus, WorkPackageStage } from 'shared';
25+
import { ChangeRequest, ClubAccount, StandardChangeRequest, validateWBS, WbsElementStatus, WorkPackageStage } from 'shared';
2626
import TasksService from '../services/tasks.services';
2727
import DescriptionBulletsService from '../services/description-bullets.services';
2828
import { seedProject } from './seed-data/projects.seed';
@@ -129,7 +129,7 @@ const performSeed: () => Promise<void> = async () => {
129129
/**
130130
* Make an initial change request for car 1 using the wbs of the genesis project
131131
*/
132-
const changeRequest1Id: number = await ChangeRequestsService.createStandardChangeRequest(
132+
const changeRequest1: StandardChangeRequest = await ChangeRequestsService.createStandardChangeRequest(
133133
cyborg,
134134
genesisProject.wbsElement.carNumber,
135135
genesisProject.wbsElement.projectNumber,
@@ -141,21 +141,25 @@ const performSeed: () => Promise<void> = async () => {
141141
type: Scope_CR_Why_Type.INITIALIZATION,
142142
explain: 'need this to initialize all the seed data'
143143
}
144+
],
145+
[
146+
{
147+
description: 'Initialize seed data',
148+
scopeImpact: 'no scope impact',
149+
timelineImpact: 0,
150+
budgetImpact: 0
151+
}
144152
]
145153
);
146154

147-
// make a proposed solution for it
148-
const proposedSolution1Id: string = await ChangeRequestsService.addProposedSolution(
149-
cyborg,
150-
changeRequest1Id,
151-
0,
152-
'Initializing seed data',
153-
0,
154-
'no scope impact'
155-
);
156-
157155
// approve the change request
158-
await ChangeRequestsService.reviewChangeRequest(batman, changeRequest1Id, 'LGTM', true, proposedSolution1Id);
156+
await ChangeRequestsService.reviewChangeRequest(
157+
batman,
158+
changeRequest1.crId,
159+
'LGTM',
160+
true,
161+
changeRequest1.proposedSolutions[0].id
162+
);
159163

160164
/**
161165
* TEAMS
@@ -262,7 +266,7 @@ const performSeed: () => Promise<void> = async () => {
262266
/** Project 1 */
263267
const { projectWbsNumber: project1WbsNumber, projectId: project1Id } = await seedProject(
264268
thomasEmrax,
265-
changeRequest1Id,
269+
changeRequest1.crId,
266270
1,
267271
'Impact Attenuator',
268272
'Develop rules-compliant impact attenuator',
@@ -292,7 +296,7 @@ const performSeed: () => Promise<void> = async () => {
292296
/** Project 2 */
293297
const { projectWbsNumber: project2WbsNumber, projectId: project2Id } = await seedProject(
294298
thomasEmrax,
295-
changeRequest1Id,
299+
changeRequest1.crId,
296300
1,
297301
'Bodywork',
298302
'Develop rules-compliant bodywork',
@@ -322,7 +326,7 @@ const performSeed: () => Promise<void> = async () => {
322326
/** Project 3 */
323327
const { projectWbsNumber: project3WbsNumber, projectId: project3Id } = await seedProject(
324328
thomasEmrax,
325-
changeRequest1Id,
329+
changeRequest1.crId,
326330
1,
327331
'Battery Box',
328332
'Develop rules-compliant battery box.',
@@ -352,7 +356,7 @@ const performSeed: () => Promise<void> = async () => {
352356
/** Project 4 */
353357
const { projectWbsNumber: project4WbsNumber, projectId: project4Id } = await seedProject(
354358
thomasEmrax,
355-
changeRequest1Id,
359+
changeRequest1.crId,
356360
1,
357361
'Motor Controller Integration',
358362
'Develop rules-compliant motor controller integration.',
@@ -382,7 +386,7 @@ const performSeed: () => Promise<void> = async () => {
382386
/** Project 5 */
383387
const { projectWbsNumber: project5WbsNumber, projectId: project5Id } = await seedProject(
384388
thomasEmrax,
385-
changeRequest1Id,
389+
changeRequest1.crId,
386390
1,
387391
'Wiring Harness',
388392
'Develop rules-compliant wiring harness.',
@@ -417,7 +421,7 @@ const performSeed: () => Promise<void> = async () => {
417421
joeShmoe,
418422
project1WbsNumber,
419423
'Bodywork Concept of Design',
420-
changeRequest1Id,
424+
changeRequest1.crId,
421425
WorkPackageStage.Design,
422426
'01/01/2023',
423427
3,
@@ -458,7 +462,7 @@ const performSeed: () => Promise<void> = async () => {
458462
thomasEmrax,
459463
project1WbsNumber,
460464
'Adhesive Shear Strength Test',
461-
changeRequest1Id,
465+
changeRequest1.crId,
462466
WorkPackageStage.Research,
463467
'01/22/2023',
464468
5,
@@ -482,7 +486,7 @@ const performSeed: () => Promise<void> = async () => {
482486
thomasEmrax,
483487
project5WbsNumber,
484488
'Manufacture Wiring Harness',
485-
changeRequest1Id,
489+
changeRequest1.crId,
486490
WorkPackageStage.Manufacturing,
487491
'02/01/2023',
488492
3,
@@ -502,7 +506,7 @@ const performSeed: () => Promise<void> = async () => {
502506
thomasEmrax,
503507
project5WbsNumber,
504508
'Install Wiring Harness',
505-
changeRequest1Id,
509+
changeRequest1.crId,
506510
WorkPackageStage.Install,
507511
'04/01/2023',
508512
7,
@@ -527,7 +531,7 @@ const performSeed: () => Promise<void> = async () => {
527531
true
528532
);
529533

530-
const changeRequest2Id = await ChangeRequestsService.createStandardChangeRequest(
534+
const changeRequest2 = await ChangeRequestsService.createStandardChangeRequest(
531535
thomasEmrax,
532536
project2WbsNumber.carNumber,
533537
project2WbsNumber.projectNumber,
@@ -537,18 +541,23 @@ const performSeed: () => Promise<void> = async () => {
537541
[
538542
{ type: Scope_CR_Why_Type.DESIGN, explain: 'It would be really pretty' },
539543
{ type: Scope_CR_Why_Type.ESTIMATION, explain: 'I estimate that it would be really pretty' }
544+
],
545+
[
546+
{
547+
description: 'Buy hot pink paint',
548+
scopeImpact: 'n/a',
549+
timelineImpact: 1,
550+
budgetImpact: 50
551+
},
552+
{
553+
description: 'Buy slightly cheaper but lower quality hot pink paint',
554+
scopeImpact: 'n/a',
555+
timelineImpact: 1,
556+
budgetImpact: 40
557+
}
540558
]
541559
);
542-
await ChangeRequestsService.addProposedSolution(thomasEmrax, changeRequest2Id, 50, 'Buy hot pink paint', 1, 'n/a');
543-
await ChangeRequestsService.addProposedSolution(
544-
thomasEmrax,
545-
changeRequest2Id,
546-
40,
547-
'Buy slightly cheaper but lower quality hot pink paint',
548-
1,
549-
'n/a'
550-
);
551-
await ChangeRequestsService.reviewChangeRequest(joeShmoe, changeRequest2Id, 'What the hell Thomas', false, null);
560+
await ChangeRequestsService.reviewChangeRequest(joeShmoe, changeRequest2.crId, 'What the hell Thomas', false, null);
552561

553562
await ChangeRequestsService.createActivationChangeRequest(
554563
thomasEmrax,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ changeRequestsRouter.post(
6161
body('why').isArray(),
6262
nonEmptyString(body('why.*.explain')),
6363
body('why.*.type').custom((value) => Object.values(ChangeRequestReason).includes(value)),
64+
body('proposedSolutions').isArray({ min: 1 }),
65+
nonEmptyString(body('proposedSolutions.*.description')),
66+
nonEmptyString(body('proposedSolutions.*.scopeImpact')),
67+
body('proposedSolutions.*.timelineImpact').isInt(),
68+
body('proposedSolutions.*.budgetImpact').isInt(),
6469
validateInputs,
6570
ChangeRequestsController.createStandardChangeRequest
6671
);

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

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
1-
import { ChangeRequest, isAdmin, isGuest, isLeadership, isNotLeadership, wbsPipe } from 'shared';
1+
import {
2+
ChangeRequest,
3+
ChangeRequestReason,
4+
ChangeRequestStatus,
5+
isAdmin,
6+
isGuest,
7+
isLeadership,
8+
isNotLeadership,
9+
ProposedSolution,
10+
ProposedSolutionCreateArgs,
11+
StandardChangeRequest,
12+
wbsPipe
13+
} from 'shared';
214
import prisma from '../prisma/prisma';
315
import changeRequestQueryArgs from '../prisma-query-args/change-requests.query-args';
416
import {
@@ -527,11 +539,15 @@ export default class ChangeRequestsService {
527539
workPackageNumber: number,
528540
type: CR_Type,
529541
what: string,
530-
why: { type: Scope_CR_Why_Type; explain: string }[]
531-
): Promise<number> {
542+
why: { type: Scope_CR_Why_Type; explain: string }[],
543+
proposedSolutions: ProposedSolutionCreateArgs[]
544+
): Promise<StandardChangeRequest> {
532545
// verify user is allowed to create standard change requests
533546
if (isGuest(submitter.role)) throw new AccessDeniedGuestException('create standard change requests');
534547

548+
//verify proposed solutions length is greater than 0
549+
if (proposedSolutions.length === 0) throw new HttpException(400, 'No proposed solutions provided');
550+
535551
// verify wbs element exists
536552
const wbsElement = await prisma.wBS_Element.findUnique({
537553
where: {
@@ -576,26 +592,54 @@ export default class ChangeRequestsService {
576592
}
577593
});
578594

595+
const proposedSolutionPromises = proposedSolutions.map(async (proposedSolution) => {
596+
return await this.addProposedSolution(
597+
submitter,
598+
createdCR.crId,
599+
proposedSolution.budgetImpact,
600+
proposedSolution.description,
601+
proposedSolution.timelineImpact,
602+
proposedSolution.scopeImpact
603+
);
604+
});
605+
606+
const createdProposedSolutions = await Promise.all(proposedSolutionPromises);
607+
579608
const project = createdCR.wbsElement.workPackage?.project || createdCR.wbsElement.project;
580609
const teams = project?.teams;
581610
if (teams && teams.length > 0) {
582611
const completion: Promise<void>[] = teams.map(async (team) => {
583612
const slackMsg =
584613
`${type} CR submitted by ${submitter.firstName} ${submitter.lastName} ` +
585614
`for the ${project.wbsElement.name} project`;
586-
try {
587-
await sendSlackChangeRequestNotification(team, slackMsg, createdCR.crId);
588-
} catch (error) {
589-
if (error instanceof Error) {
590-
throw new HttpException(511, `Failed to send slack notification for CR: ${createdCR.crId}`);
591-
}
592-
}
615+
await sendSlackChangeRequestNotification(team, slackMsg, createdCR.crId);
593616
});
594617

595618
await Promise.all(completion);
596619
}
597620

598-
return createdCR.crId;
621+
return {
622+
...createdCR,
623+
what,
624+
why: why.map((why) => ({ explain: why.explain, type: why.type as ChangeRequestReason })),
625+
type,
626+
wbsNum: {
627+
carNumber,
628+
projectNumber,
629+
workPackageNumber
630+
},
631+
wbsName: wbsElement.name,
632+
submitter,
633+
status: ChangeRequestStatus.Open,
634+
requestedReviewers: [],
635+
dateReviewed: undefined,
636+
accepted: undefined,
637+
reviewNotes: undefined,
638+
scopeImpact: '',
639+
budgetImpact: 0,
640+
timelineImpact: 0,
641+
proposedSolutions: createdProposedSolutions
642+
};
599643
}
600644

601645
/**
@@ -617,7 +661,7 @@ export default class ChangeRequestsService {
617661
description: string,
618662
timelineImpact: number,
619663
scopeImpact: string
620-
): Promise<string> {
664+
): Promise<ProposedSolution> {
621665
// verify user is allowed to add proposed solutions
622666
if (isGuest(submitter.role)) throw new AccessDeniedGuestException('add proposed solutions');
623667

@@ -643,10 +687,11 @@ export default class ChangeRequestsService {
643687
budgetImpact,
644688
changeRequest: { connect: { scopeCrId: foundScopeCR.scopeCrId } },
645689
createdBy: { connect: { userId: submitter.userId } }
646-
}
690+
},
691+
include: { createdBy: true }
647692
});
648693

649-
return createProposedSolution.proposedSolutionId;
694+
return { ...createProposedSolution, id: createProposedSolution.proposedSolutionId };
650695
}
651696

652697
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const sendSlackChangeRequestNotification = async (
2828
crId: number,
2929
budgetImpact?: number
3030
) => {
31-
if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
31+
// if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
3232
const msgs = [];
3333
const fullMsg = `:tada: New Change Request! :tada: ${message}`;
3434
const fullLink = `https://finishlinebyner.com/cr/${crId}`;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ describe('Change Requests', () => {
4141
return undefined;
4242
});
4343
vi.spyOn(changeRequestUtils, 'sendSlackChangeRequestNotification').mockImplementation(async (_slackId, _crId) => {
44-
return undefined;
44+
return [];
4545
});
4646
vi.spyOn(changeRequestUtils, 'updateBlocking').mockImplementation(async () => {});
4747
vi.spyOn(prisma.user_Settings, 'findUnique').mockResolvedValueOnce(batmanSettings);
@@ -344,7 +344,7 @@ describe('Change Requests', () => {
344344
vi.spyOn(prisma.scope_CR, 'findUnique').mockResolvedValue(prismaScopeChangeRequest1);
345345
vi.spyOn(prisma.proposed_Solution, 'create').mockResolvedValue(prismaProposedSolution1);
346346
const response = await ChangeRequestsService.addProposedSolution(aquaman, crId, 1000, description, 10, 'huge');
347-
expect(response).toStrictEqual(prismaProposedSolution1.proposedSolutionId);
347+
expect(response).toStrictEqual({...prismaProposedSolution1, id: prismaProposedSolution1.proposedSolutionId});
348348
expect(prisma.change_Request.findUnique).toHaveBeenCalledTimes(1);
349349
expect(prisma.scope_CR.findUnique).toHaveBeenCalledTimes(1);
350350
expect(prisma.proposed_Solution.create).toHaveBeenCalledTimes(1);

0 commit comments

Comments
 (0)