Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
7102b74
#4168 2 migrations + changes everywhere to get rid of unused function…
wavehassman Apr 21, 2026
7b5107c
feature branch
wavehassman Apr 21, 2026
9a181dc
#4168 combined migration files
wavehassman Apr 22, 2026
a153a55
#4168 requested changes
wavehassman Apr 24, 2026
67a5de3
Merge pull request #4181 from Northeastern-Electric-Racing/#4168-sche…
wavehassman Apr 26, 2026
9f82982
#4170 updated service and added test
wavehassman Apr 26, 2026
567df82
#4170 allow heads and above to review their own crs
wavehassman Apr 26, 2026
2b715d2
Merge pull request #4185 from Northeastern-Electric-Racing/#4170-appr…
wavehassman Apr 26, 2026
44fc98b
#4171 diff calc and added why + changes to notif
cielbellerose Apr 27, 2026
e42d414
#4171 added project link diffs
cielbellerose Apr 27, 2026
e96d215
#4173 made reviewer a user, fixed up change request form
wavehassman Apr 27, 2026
3265598
#4174 removed old cr stuff and fixed up all the frontend
wavehassman Apr 28, 2026
93ea2b7
#4174 added check on reviewer id and added bullets to diff
wavehassman Apr 28, 2026
9ea9b59
#4174 fix tests
wavehassman Apr 28, 2026
838f8bf
#4176 added dateCompleted to stage gate
wavehassman Apr 30, 2026
cc998cd
#4176 cant mark an end date before the start date
wavehassman Apr 30, 2026
db4e3b4
#4176 put error above loading
wavehassman Apr 30, 2026
a04aa33
Merge remote-tracking branch 'origin/feature/descoping-cr' into #4171…
cielbellerose May 1, 2026
d5cdc6f
#4171 add wp deliverables to diff
cielbellerose May 2, 2026
9db010b
Merge pull request #4192 from Northeastern-Electric-Racing/#4173-cr-form
wavehassman May 3, 2026
9c7ff1a
#4174 added toast, fixed button formatting
wavehassman May 3, 2026
2d76eb1
#4176 removed newDuration
wavehassman May 3, 2026
e6b2661
Merge pull request #4190 from Northeastern-Electric-Racing/#4171-slac…
wavehassman May 3, 2026
eb6fbc8
Merge pull request #4194 from Northeastern-Electric-Racing/#4176-stag…
wavehassman May 3, 2026
83d971e
#4174 merge conflicts
wavehassman May 5, 2026
4f316f7
#4174 made reviewer optional, do not need to make a change rquest whe…
wavehassman May 8, 2026
f94c8d7
#4172 started cr approve slack button
staysgt May 8, 2026
c577506
Merge pull request #4193 from Northeastern-Electric-Racing/#4174-remo…
wavehassman May 8, 2026
ba4baaa
#4172 fix lint
staysgt May 12, 2026
0b3225a
#4172 fix build issue
staysgt May 12, 2026
94e7fe2
#4172 fix build
staysgt May 12, 2026
0ad7c39
#4172 fix build hopefully
wavehassman May 13, 2026
cc8ad81
Merge branch 'feature/descoping-cr' into #4172-cr-slack-button
wavehassman May 13, 2026
19d4058
#4172 merged in new changes
wavehassman May 13, 2026
de2130e
#4172 fix from review
staysgt May 13, 2026
776c52f
Merge pull request #4202 from Northeastern-Electric-Racing/#4172-cr-s…
wavehassman May 13, 2026
3b8d9d9
merge into develop
wavehassman May 13, 2026
b0b5ee8
Merge branch 'develop' into feature/descoping-cr
wavehassman May 13, 2026
e54a70f
fix: update e2e tests for descoped CR workflow
wavehassman May 13, 2026
8d3c591
hopefully works now
wavehassman May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 8 additions & 31 deletions src/backend/src/controllers/change-requests.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,13 @@ export default class ChangeRequestsController {

static async reviewChangeRequest(req: Request, res: Response, next: NextFunction) {
try {
const { crId, reviewNotes, accepted, psId } = req.body;
const { crId, reviewNotes, accepted } = req.body;
const id = await ChangeRequestsService.reviewChangeRequest(
req.currentUser,
crId,
reviewNotes,
accepted,
req.organization,
psId
reviewNotes
);
res.status(200).json({ message: `Change request #${id} successfully reviewed.` });
} catch (error: unknown) {
Expand All @@ -100,14 +99,13 @@ export default class ChangeRequestsController {

static async createActivationChangeRequest(req: Request, res: Response, next: NextFunction) {
try {
const { wbsNum, type, leadId, managerId, startDate, confirmDetails } = req.body;
const { wbsNum, leadId, managerId, startDate, confirmDetails } = req.body;

const id = await ChangeRequestsService.createActivationChangeRequest(
req.currentUser,
wbsNum.carNumber,
wbsNum.projectNumber,
wbsNum.workPackageNumber,
type,
leadId,
managerId,
startDate,
Expand All @@ -122,14 +120,14 @@ export default class ChangeRequestsController {

static async createStageGateChangeRequest(req: Request, res: Response, next: NextFunction) {
try {
const { wbsNum, type, confirmDone } = req.body;
const { wbsNum, confirmDone, dateCompleted } = req.body;
const id = await ChangeRequestsService.createStageGateChangeRequest(
req.currentUser,
wbsNum.carNumber,
wbsNum.projectNumber,
wbsNum.workPackageNumber,
type,
confirmDone,
new Date(dateCompleted),
req.organization
);
res.status(200).json({ message: `Successfully created stage gate request with id #${id}` });
Expand All @@ -140,10 +138,9 @@ export default class ChangeRequestsController {

static async createBudgetChangeRequest(req: Request, res: Response, next: NextFunction) {
try {
const { otherReasonId, accountCodeId, type, proposedBudget } = req.body;
const { otherReasonId, accountCodeId, proposedBudget } = req.body;
const cr = await ChangeRequestsService.createBudgetChangeRequest(
req.currentUser,
type,
proposedBudget,
req.organization,
otherReasonId,
Expand Down Expand Up @@ -176,7 +173,7 @@ export default class ChangeRequestsController {

static async createStandardChangeRequest(req: Request, res: Response, next: NextFunction) {
try {
const { wbsNum, type, what, why, proposedSolutions, projectProposedChanges, workPackageProposedChanges } = req.body;
const { wbsNum, why, requestedReviewerId, projectProposedChanges, workPackageProposedChanges } = req.body;
if (workPackageProposedChanges && workPackageProposedChanges.stage === 'NONE') {
workPackageProposedChanges.stage = null;
}
Expand All @@ -186,11 +183,9 @@ export default class ChangeRequestsController {
wbsNum.carNumber,
wbsNum.projectNumber,
wbsNum.workPackageNumber,
type,
what,
why,
proposedSolutions,
req.organization,
requestedReviewerId,
projectProposedChanges,
workPackageProposedChanges
);
Expand All @@ -200,24 +195,6 @@ export default class ChangeRequestsController {
}
}

static async addProposedSolution(req: Request, res: Response, next: NextFunction) {
try {
const { crId, budgetImpact, description, timelineImpact, scopeImpact } = req.body;
const id = await ChangeRequestsService.addProposedSolution(
req.currentUser,
crId,
budgetImpact,
description,
timelineImpact,
scopeImpact,
req.organization
);
res.status(200).json({ message: `Successfully added proposed solution with id #${id}` });
} catch (error: unknown) {
next(error);
}
}

static async deleteChangeRequest(req: Request, res: Response, next: NextFunction) {
try {
const { crId } = req.params as Record<string, string>;
Expand Down
74 changes: 73 additions & 1 deletion src/backend/src/controllers/slack.controllers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { getWorkspaceId, replyToMessageInThread } from '../integrations/slack.js';
import OrganizationsService from '../services/organizations.services.js';
import SlackServices, { SlackBlockActionBody, SaboSubmissionActionValue } from '../services/slack.services.js';
import SlackServices, {
SlackBlockActionBody,
SaboSubmissionActionValue,
CrApprovalActionValue
} from '../services/slack.services.js';

export default class SlackController {
static async processMessageEvent(event: any) {
Expand Down Expand Up @@ -82,4 +86,72 @@ export default class SlackController {
throw error;
}
}

static async handleApproveCRAction(
body: SlackBlockActionBody,
respond: (msg: {
response_type?: 'ephemeral';
text?: string;
replace_original?: boolean;
delete_original?: boolean;
}) => Promise<unknown>
) {
const { user, container, actions } = body;
const channelId = container.channel_id;
const threadTs = container.thread_ts || container.message_ts;
const [firstAction] = actions;

try {
// Action-specific validation: verify action_id
if (firstAction.action_id !== 'approve_cr') {
console.error('Unexpected action_id:', firstAction.action_id);
await replyToMessageInThread(
channelId,
threadTs,
`❌ An error occurred: Unexpected action type "${firstAction.action_id}". Please contact the software team.`
);
return;
}

// Action-specific validation: verify value format
let actionValue: CrApprovalActionValue;
try {
actionValue = JSON.parse(firstAction.value);
} catch (parseError) {
const parseErrorMsg = parseError instanceof Error ? parseError.message : 'Unknown parse error';
await replyToMessageInThread(
channelId,
threadTs,
`❌ An error occurred: Invalid action data format.\n\n*Error:* ${parseErrorMsg}\n*Value:* \`${firstAction.value}\`\n\nPlease contact the software team.`
);
return;
}

// Validate that changeRequestId exists in the parsed value
if (!actionValue.crId || typeof actionValue.crId !== 'string') {
const actionValueStr = JSON.stringify(actionValue, null, 2);
await replyToMessageInThread(
channelId,
threadTs,
`❌ An error occurred: Missing or invalid reimbursement request ID.\n\n*Parsed value:*\n\`\`\`${actionValueStr}\`\`\`\n\nPlease contact the software team.`
);
return;
}

// Extract validated fields
const userSlackId = user.id;
const { crId } = actionValue;

// Pass the extracted fields to the service layer for business logic
await SlackServices.handleApproveCRAction(userSlackId, crId, respond);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
await replyToMessageInThread(
channelId,
threadTs,
`❌ An unexpected error occurred while processing your request.\n\n*Error message:* ${errorMessage}\n\nPlease contact the software team and provide them with this information.`
);
throw error;
}
}
}
92 changes: 66 additions & 26 deletions src/backend/src/prisma-query-args/change-requests.query-args.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,73 @@
import { Prisma } from '@prisma/client';
import { getScopeChangeRequestQueryArgs } from './scope-change-requests.query-args.js';
import { getUserQueryArgs } from './user.query-args.js';
import { getWorkPackageQueryArgs } from './work-packages.query-args.js';
import { getReimbursementProductOtherReasonQueryArgs } from './reimbursement-product-other-reason.query-args.js';
import { getAccountCodeQueryArgs } from './account-code.query-args.js';
import { getTeamQueryArgs } from './teams.query-args.js';

export type ChangeRequestQueryArgs = ReturnType<typeof getChangeRequestQueryArgs>;

export type ChangeRequestWithProjectAndWorkPackageQueryArgs = ReturnType<
typeof getChangeRequestWithProjectAndWorkPackageQueryArgs
>;

export type WbsProposedChangeQueryArgs = ReturnType<typeof getWbsProposedChangesQueryArgs>;
export type ChangeRequestManyQueryArgs = ReturnType<typeof getManyChangeRequestQueryArgs>;
export type WorkPackageProposedChangesQueryArgs = ReturnType<typeof getWorkPackageProposedChangesQueryArgs>;
export type ProjectProposedChangesQueryArgs = ReturnType<typeof getProjectProposedChangesQueryArgs>;

const getProjectProposedChangesQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.Project_Proposed_ChangesDefaultArgs>()({
include: {
teams: getTeamQueryArgs(organizationId),
car: { include: { wbsElement: true } },
workPackageProposedChanges: getWorkPackageProposedChangesQueryArgs(organizationId)
}
});

const getWorkPackageProposedChangesQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.Work_Package_Proposed_ChangesDefaultArgs>()({
include: {
wbsProposedChanges: {
include: {
links: { where: { dateDeleted: null }, include: { linkType: true } },
proposedDescriptionBulletChanges: {
where: { dateDeleted: null },
include: {
descriptionBulletType: true,
userChecked: getUserQueryArgs(organizationId)
}
},
lead: getUserQueryArgs(organizationId),
manager: getUserQueryArgs(organizationId)
}
},
blockedBy: true
}
});

const getWbsProposedChangesQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.Wbs_Proposed_ChangesDefaultArgs>()({
include: {
links: { where: { dateDeleted: null }, include: { linkType: true } },
proposedDescriptionBulletChanges: {
where: { dateDeleted: null },
include: { descriptionBulletType: true, userChecked: getUserQueryArgs(organizationId) }
},
lead: getUserQueryArgs(organizationId),
manager: getUserQueryArgs(organizationId),
projectProposedChanges: {
include: {
teams: getTeamQueryArgs(organizationId),
car: {
include: {
wbsElement: true
}
},
workPackageProposedChanges: getWorkPackageProposedChangesQueryArgs(organizationId)
}
},
workPackageProposedChanges: getWorkPackageProposedChangesQueryArgs(organizationId)
}
});

export const getChangeRequestQueryArgs = (organizationId: string) =>
Prisma.validator<Prisma.Change_RequestDefaultArgs>()({
Expand All @@ -22,17 +78,13 @@ export const getChangeRequestQueryArgs = (organizationId: string) =>
accountCode: getAccountCodeQueryArgs(organizationId),
reviewer: getUserQueryArgs(organizationId),
changes: {
where: {
wbsElement: {
dateDeleted: null
}
},
where: { wbsElement: { dateDeleted: null } },
include: {
implementer: getUserQueryArgs(organizationId),
wbsElement: true
}
},
scopeChangeRequest: getScopeChangeRequestQueryArgs(organizationId),
wbsProposedChanges: getWbsProposedChangesQueryArgs(organizationId),
stageGateChangeRequest: true,
activationChangeRequest: {
include: { lead: getUserQueryArgs(organizationId), manager: getUserQueryArgs(organizationId) }
Expand Down Expand Up @@ -91,19 +143,15 @@ export const getGuestChangeRequestQueryArgs = (organizationId: string) =>
project: {
select: {
wbsElement: { select: { name: true } },
teams: {
select: { teamType: { select: { name: true } } }
}
teams: { select: { teamType: { select: { name: true } } } }
}
},
workPackage: {
select: {
project: {
select: {
wbsElement: { select: { name: true } },
teams: {
select: { teamType: { select: { name: true } } }
}
teams: { select: { teamType: { select: { name: true } } } }
}
}
}
Expand All @@ -120,11 +168,7 @@ export const getChangeRequestWithProjectAndWorkPackageQueryArgs = (organizationI
wbsElement: {
include: {
workPackage: getWorkPackageQueryArgs(organizationId),
project: {
include: {
teams: true
}
},
project: { include: { teams: true } },
descriptionBullets: { where: { dateDeleted: null } },
links: { where: { dateDeleted: null } }
}
Expand All @@ -133,19 +177,15 @@ export const getChangeRequestWithProjectAndWorkPackageQueryArgs = (organizationI
accountCode: getAccountCodeQueryArgs(organizationId),
reviewer: getUserQueryArgs(organizationId),
changes: {
where: {
wbsElement: {
dateDeleted: null
}
},
where: { wbsElement: { dateDeleted: null } },
include: {
implementer: getUserQueryArgs(organizationId),
wbsElement: true,
category: getReimbursementProductOtherReasonQueryArgs(organizationId),
accountCode: getAccountCodeQueryArgs(organizationId)
}
},
scopeChangeRequest: getScopeChangeRequestQueryArgs(organizationId),
wbsProposedChanges: getWbsProposedChangesQueryArgs(organizationId),
stageGateChangeRequest: true,
activationChangeRequest: {
include: { lead: getUserQueryArgs(organizationId), manager: getUserQueryArgs(organizationId) }
Expand Down
11 changes: 0 additions & 11 deletions src/backend/src/prisma-query-args/proposed-solutions.query-args.ts

This file was deleted.

Loading
Loading