Skip to content

Commit 9da0bc0

Browse files
authored
Merge branch 'develop' into #1277-only-proj-reimbursement
2 parents d79723d + b98a441 commit 9da0bc0

58 files changed

Lines changed: 791 additions & 303 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A project management web application built in Typescript, React, and Express.
88

99
All of our documentation is on [our Confluence page](https://nerdocs.atlassian.net/wiki/spaces/NER/pages/5603329/Software). Start there to learn what the project is all about.
1010

11-
All questions can be directed to `#software` in the [NER Slack](https://nu-electric-racing.slack.com) (backup contact: [`@anthonybernardi`](https://github.com/anthonybernardi)).
11+
All questions can be directed to `#software` in the [NER Slack](https://nu-electric-racing.slack.com) (backup contact: [`@RChandler234`](https://github.com/RChandler234)).
1212

1313
## Environment Setup
1414

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ export default class ReimbursementRequestsController {
7272
static async reimburseUser(req: Request, res: Response, next: NextFunction) {
7373
try {
7474
const user = await getCurrentUser(res);
75-
const { amount } = req.body;
75+
const { amount, dateReceived } = req.body;
7676

77-
const reimbursement = await ReimbursementRequestService.reimburseUser(amount, user);
77+
const reimbursement = await ReimbursementRequestService.reimburseUser(amount, dateReceived, user);
7878
res.status(200).json(reimbursement);
7979
} catch (error: unknown) {
8080
next(error);

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ reimbursementRequestsRouter.post(
111111
reimbursementRequestsRouter.post(
112112
'/reimburse',
113113
intMinZero(body('amount')),
114+
isDate(body('dateReceived')),
114115
validateInputs,
115116
ReimbursementRequestController.reimburseUser
116117
);

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,11 @@ export default class ReimbursementRequestService {
167167
* Function to reimburse a user for their expenses.
168168
*
169169
* @param amount the amount to be reimbursed
170+
* @param dateReceived the date the amount was received
170171
* @param submitter the person performing the reimbursement
171172
* @returns the created reimbursement
172173
*/
173-
static async reimburseUser(amount: number, submitter: User): Promise<Reimbursement> {
174+
static async reimburseUser(amount: number, dateReceived: string, submitter: User): Promise<Reimbursement> {
174175
if (isGuest(submitter.role)) {
175176
throw new AccessDeniedException('Guests cannot reimburse a user for their expenses.');
176177
}
@@ -195,11 +196,16 @@ export default class ReimbursementRequestService {
195196
if (amount > totalOwed - totalReimbursed) {
196197
throw new HttpException(400, 'Reimbursement is greater than the total amount owed to the user');
197198
}
199+
200+
// make the date object but add 12 hours so that the time isn't 00:00 to avoid timezone problems
201+
const dateCreated = new Date(dateReceived.split('T')[0]);
202+
dateCreated.setTime(dateCreated.getTime() + 12 * 60 * 60 * 1000);
203+
198204
const newReimbursement = await prisma.reimbursement.create({
199205
data: {
200206
purchaserId: submitter.userId,
201207
amount,
202-
dateCreated: new Date(),
208+
dateCreated: dateReceived,
203209
userSubmittedId: submitter.userId
204210
},
205211
...reimbursementQueryArgs

src/backend/src/services/tasks.services.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import teamQueryArgs from '../prisma-query-args/teams.query-args';
66
import prisma from '../prisma/prisma';
77
import taskTransformer from '../transformers/tasks.transformer';
88
import { NotFoundException, AccessDeniedException, HttpException, DeletedException } from '../utils/errors.utils';
9-
import { hasPermissionToEditTask } from '../utils/tasks.utils';
9+
import { hasPermissionToEditTask, sendSlackTaskAssignedNotificationToUsers } from '../utils/tasks.utils';
1010
import { areUsersPartOfTeams, isUserOnTeam } from '../utils/teams.utils';
1111
import { getUsers } from '../utils/users.utils';
1212
import { wbsNumOf } from '../utils/utils';
@@ -79,7 +79,11 @@ export default class TasksService {
7979
...taskQueryArgs
8080
});
8181

82-
return taskTransformer(createdTask);
82+
const newTask = taskTransformer(createdTask);
83+
84+
sendSlackTaskAssignedNotificationToUsers(newTask, assignees);
85+
86+
return newTask;
8387
}
8488

8589
/**
@@ -94,7 +98,10 @@ export default class TasksService {
9498
*/
9599
static async editTask(user: User, taskId: string, title: string, notes: string, priority: Task_Priority, deadline: Date) {
96100
const hasPermission = await hasPermissionToEditTask(user, taskId);
97-
if (!hasPermission) throw new AccessDeniedException();
101+
if (!hasPermission)
102+
throw new AccessDeniedException(
103+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
104+
);
98105

99106
const originalTask = await prisma.task.findUnique({ where: { taskId } });
100107
if (!originalTask) throw new NotFoundException('Task', taskId);
@@ -129,7 +136,7 @@ export default class TasksService {
129136
const hasPermission = await hasPermissionToEditTask(user, taskId);
130137
if (!hasPermission)
131138
throw new AccessDeniedException(
132-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
139+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
133140
);
134141

135142
const updatedTask = await prisma.task.update({ where: { taskId }, data: { status }, ...taskQueryArgs });
@@ -149,16 +156,20 @@ export default class TasksService {
149156
const originalTask = await prisma.task.findUnique({
150157
where: { taskId },
151158
include: {
152-
wbsElement: { include: { project: { ...projectQueryArgs } } }
159+
wbsElement: { include: { project: { ...projectQueryArgs } } },
160+
assignees: true
153161
}
154162
});
155163
if (!originalTask) throw new NotFoundException('Task', taskId);
156164
if (originalTask.dateDeleted) throw new DeletedException('Task', taskId);
157165

166+
const originalAssigneeIds = originalTask.assignees.map((assignee) => assignee.userId);
167+
const newAssigneeIds = assignees.filter((userId) => !originalAssigneeIds.includes(userId));
168+
158169
const hasPermission = await hasPermissionToEditTask(user, taskId);
159170
if (!hasPermission)
160171
throw new AccessDeniedException(
161-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
172+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
162173
);
163174

164175
// this throws if any of the users aren't found
@@ -180,17 +191,21 @@ export default class TasksService {
180191
};
181192
});
182193

183-
const updatedTask = await prisma.task.update({
184-
where: { taskId },
185-
data: {
186-
assignees: {
187-
set: transformedAssigneeUsers
188-
}
189-
},
190-
...taskQueryArgs
191-
});
192-
193-
return taskTransformer(updatedTask);
194+
const updatedTask = taskTransformer(
195+
await prisma.task.update({
196+
where: { taskId },
197+
data: {
198+
assignees: {
199+
set: transformedAssigneeUsers
200+
}
201+
},
202+
...taskQueryArgs
203+
})
204+
);
205+
206+
await sendSlackTaskAssignedNotificationToUsers(updatedTask, newAssigneeIds);
207+
208+
return updatedTask;
194209
}
195210

196211
/**

src/backend/src/services/work-packages.services.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ export default class WorkPackagesService {
154154
throw new HttpException(400, 'A Work Package cannot have its own project as a blocker');
155155
}
156156

157+
blockedBy.forEach((dep: WbsNumber) => {
158+
if (dep.workPackageNumber === 0) {
159+
throw new HttpException(400, 'A Project cannot be a Blocker');
160+
}
161+
});
162+
157163
const wbsElem = await prisma.wBS_Element.findUnique({
158164
where: {
159165
wbsNumber: {
@@ -287,6 +293,12 @@ export default class WorkPackagesService {
287293
// verify user is allowed to edit work packages
288294
if (isGuest(user.role)) throw new AccessDeniedGuestException('edit work packages');
289295

296+
blockedBy.forEach((dep: WbsNumber) => {
297+
if (dep.workPackageNumber === 0) {
298+
throw new HttpException(400, 'A Project cannot be a Blocker');
299+
}
300+
});
301+
290302
const { userId } = user;
291303

292304
// get the original work package so we can compare things

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { ChangeRequest, daysBetween, User, wbsPipe, WorkPackage } from 'shared';
1+
import { ChangeRequest, daysBetween, Task, User, wbsPipe, WorkPackage } from 'shared';
22
import { sendMessage } from '../integrations/slack';
33
import { getUserSlackId } from './users.utils';
4+
import prisma from '../prisma/prisma';
45

56
// build the "due" string for the upcoming deadlines slack message
67
const buildDueString = (daysUntilDeadline: number): string => {
@@ -51,3 +52,18 @@ export const sendSlackRequestedReviewNotification = async (slackId: string, chan
5152
const fullMsg = `Your review has been requested on CR #${changeRequest.crId}: ${changeRequestLink}.`;
5253
await sendMessage(slackId, fullMsg);
5354
};
55+
56+
/**
57+
* Send Task assigned notification to assignee on Slack
58+
* @param slackId the slack id of the assignee
59+
* @param task the task they were assigned to
60+
*/
61+
export const sendSlackTaskAssignedNotification = async (slackId: string, task: Task): Promise<void> => {
62+
if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
63+
64+
const project = await prisma.wBS_Element.findUnique({ where: { wbsNumber: task.wbsNum } });
65+
const msg = `You have been assigned to a task: ${task.title} on project ${wbsPipe(task.wbsNum)} - ${project?.name}`;
66+
const link = `https://finishlinebyner.com/projects/${wbsPipe(task.wbsNum)}/tasks`;
67+
const linkButtonText = 'View Task';
68+
await sendMessage(slackId, msg, link, linkButtonText);
69+
};

src/backend/src/utils/tasks.utils.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Task_Priority, Task_Status, User } from '@prisma/client';
2-
import { isHead, TaskPriority, TaskStatus } from 'shared';
2+
import { isHead, Task, TaskPriority, TaskStatus } from 'shared';
33
import prisma from '../prisma/prisma';
4+
import { sendSlackTaskAssignedNotification } from './slack.utils';
45

56
export const convertTaskPriority = (priority: Task_Priority): TaskPriority =>
67
({
@@ -103,3 +104,17 @@ export const hasPermissionToEditTask = async (user: User, taskId: string): Promi
103104

104105
return false;
105106
};
107+
108+
/**
109+
* Sends a task assigned notification to the specified users on Slack
110+
* @param task the task the users are assigned to
111+
* @param assigneeIds the user ids of the users assigned to the task
112+
*/
113+
export const sendSlackTaskAssignedNotificationToUsers = async (task: Task, assigneeIds: number[]) => {
114+
const assigneeSettings = await prisma.user_Settings.findMany({ where: { userId: { in: assigneeIds } } });
115+
assigneeSettings.forEach(async (settings) => {
116+
if (settings.slackId) {
117+
await sendSlackTaskAssignedNotification(settings.slackId, task);
118+
}
119+
});
120+
};

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ describe('Reimbursement Requests', () => {
573573

574574
describe('Reimbursement User Tests', () => {
575575
test('Throws an error if user is a guest', async () => {
576-
await expect(ReimbursementRequestService.reimburseUser(100, theVisitor)).rejects.toThrow(
576+
await expect(ReimbursementRequestService.reimburseUser(100, '2023-01-11T11:12:33.409Z', theVisitor)).rejects.toThrow(
577577
new AccessDeniedException('Guests cannot reimburse a user for their expenses.')
578578
);
579579
});
@@ -590,7 +590,7 @@ describe('Reimbursement Requests', () => {
590590
}
591591
]);
592592

593-
await expect(ReimbursementRequestService.reimburseUser(200, batman)).rejects.toThrow(
593+
await expect(ReimbursementRequestService.reimburseUser(200, '2023-01-01T09:12:33.409Z', batman)).rejects.toThrow(
594594
new HttpException(400, 'Reimbursement is greater than the total amount owed to the user')
595595
);
596596
});
@@ -602,7 +602,7 @@ describe('Reimbursement Requests', () => {
602602
reimbursementId: 'reimbursementMockId',
603603
purchaserId: batman.userId,
604604
amount: reimbursementAmount,
605-
dateCreated: new Date(),
605+
dateCreated: new Date('2023-01-01'),
606606
userSubmitted: batman,
607607
userSubmittedId: batman.userId
608608
};
@@ -619,7 +619,11 @@ describe('Reimbursement Requests', () => {
619619
]);
620620
vi.spyOn(prisma.reimbursement, 'create').mockResolvedValue(reimbursementMock);
621621

622-
const newReimbursement = await ReimbursementRequestService.reimburseUser(reimbursementAmount, batman);
622+
const newReimbursement = await ReimbursementRequestService.reimburseUser(
623+
reimbursementAmount,
624+
'2023-01-01T19:12:33.409Z',
625+
batman
626+
);
623627

624628
expect(newReimbursement).toStrictEqual(reimbursementTransformer(reimbursementMock));
625629
});

src/backend/tests/tasks.test.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ import {
1616
taskSaveTheDayPrisma,
1717
taskSaveTheDayShared
1818
} from './test-data/tasks.test-data';
19-
import { aquaman, batman, greenlantern, superman, theVisitor, wonderwoman } from './test-data/users.test-data';
19+
import {
20+
aquaman,
21+
batman,
22+
batmanSettings,
23+
greenlantern,
24+
superman,
25+
supermanSettings,
26+
theVisitor,
27+
wonderwoman
28+
} from './test-data/users.test-data';
2029
import { prismaWbsElement1 } from './test-data/wbs-element.test-data';
2130
import { prismaProject1 } from './test-data/projects.test-data';
2231
import { justiceLeague, prismaTeam1 } from './test-data/teams.test-data';
@@ -129,6 +138,7 @@ describe('Tasks', () => {
129138
vi.spyOn(teamUtils, 'areUsersPartOfTeams').mockReturnValue(true);
130139
vi.spyOn(prisma.task, 'create').mockResolvedValue(taskSaveTheDayPrisma);
131140
vi.spyOn(prisma.user, 'findMany').mockResolvedValue([batman, wonderwoman]);
141+
vi.spyOn(prisma.user_Settings, 'findMany').mockResolvedValue([batmanSettings, supermanSettings]);
132142

133143
const task = await TasksService.createTask(batman, mockWBSNum, 'hellow world', '', mockDate, 'HIGH', 'DONE', [
134144
batman.userId,
@@ -201,7 +211,7 @@ describe('Tasks', () => {
201211
// Try updating from IN_PROGRESS to IN_BACKLOG
202212
await expect(() => TasksService.editTaskStatus(aquaman, taskId, Task_Status.IN_BACKLOG)).rejects.toThrow(
203213
new AccessDeniedException(
204-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
214+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
205215
)
206216
);
207217
});
@@ -216,7 +226,7 @@ describe('Tasks', () => {
216226
// Aquaman is a leader, but did not create this task
217227
await expect(() => TasksService.editTaskStatus(aquaman, taskId, Task_Status.IN_BACKLOG)).rejects.toThrow(
218228
new AccessDeniedException(
219-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
229+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
220230
)
221231
);
222232
});
@@ -243,6 +253,7 @@ describe('Tasks', () => {
243253
vi.spyOn(taskTransformer, 'default').mockReturnValue(taskSaveTheDayInProgressShared);
244254
vi.spyOn(userUtils, 'getUsers').mockResolvedValue([batman, wonderwoman]);
245255
vi.spyOn(teamUtils, 'areUsersPartOfTeams').mockReturnValue(true);
256+
vi.spyOn(prisma.user_Settings, 'findMany').mockResolvedValue([batmanSettings, supermanSettings]);
246257

247258
const taskId = '1';
248259
const userIds = [
@@ -288,7 +299,7 @@ describe('Tasks', () => {
288299
TasksService.editTaskAssignees(aquaman, taskId, [superman.userId, wonderwoman.userId])
289300
).rejects.toThrow(
290301
new AccessDeniedException(
291-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
302+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
292303
)
293304
);
294305
});
@@ -391,7 +402,11 @@ describe('Tasks', () => {
391402
vi.spyOn(taskUtils, 'hasPermissionToEditTask').mockResolvedValue(false);
392403
await expect(() =>
393404
TasksService.editTask(wonderwoman, taskId, fakeTitle, fakeNotes, fakePriority, fakeDeadline)
394-
).rejects.toThrow(new AccessDeniedException());
405+
).rejects.toThrow(
406+
new AccessDeniedException(
407+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
408+
)
409+
);
395410
});
396411

397412
test('Task not found', async () => {

0 commit comments

Comments
 (0)