Skip to content

Commit f5df9e4

Browse files
committed
Merge branch 'develop' into #1524-Delete-Material-Type-Endpoint
2 parents a4edacf + 155fb7a commit f5df9e4

19 files changed

Lines changed: 467 additions & 44 deletions

File tree

src/backend/src/controllers/projects.controllers.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,14 @@ export default class ProjectsController {
167167
manufacturerName,
168168
manufacturerPartNumber,
169169
quantity,
170-
unitName,
171170
price,
172171
subtotal,
173172
linkUrl,
174173
notes,
175174
wbsNum,
176175
assemblyId,
177-
pdmFileName
176+
pdmFileName,
177+
unitName
178178
);
179179
return res.status(200).json(material);
180180
} catch (error: unknown) {
@@ -223,4 +223,46 @@ export default class ProjectsController {
223223
next(error);
224224
}
225225
}
226+
227+
static async editMaterial(req: Request, res: Response, next: NextFunction) {
228+
try {
229+
const user = await getCurrentUser(res);
230+
const { materialId } = req.params;
231+
const {
232+
name,
233+
assemblyId,
234+
status,
235+
materialTypeName,
236+
manufacturerName,
237+
manufacturerPartNumber,
238+
pdmFileName,
239+
quantity,
240+
unitName,
241+
price,
242+
subtotal,
243+
linkUrl,
244+
notes
245+
} = req.body;
246+
const updatedMaterial = await ProjectsService.editMaterial(
247+
user,
248+
materialId,
249+
name,
250+
status,
251+
materialTypeName,
252+
manufacturerName,
253+
manufacturerPartNumber,
254+
quantity,
255+
price,
256+
subtotal,
257+
linkUrl,
258+
notes,
259+
unitName,
260+
assemblyId,
261+
pdmFileName
262+
);
263+
res.status(200).json(updatedMaterial);
264+
} catch (error: unknown) {
265+
next(error);
266+
}
267+
}
226268
}

src/backend/src/prisma/migrations/20231024124220_add_bom/migration.sql renamed to src/backend/src/prisma/migrations/20231119042617_add_bom/migration.sql

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,6 @@ CREATE TABLE "Manufacturer" (
6868
-- CreateIndex
6969
CREATE UNIQUE INDEX "Assembly_name_key" ON "Assembly"("name");
7070

71-
-- CreateIndex
72-
CREATE UNIQUE INDEX "Material_name_key" ON "Material"("name");
73-
7471
-- AddForeignKey
7572
ALTER TABLE "Assembly" ADD CONSTRAINT "Assembly_userDeletedId_fkey" FOREIGN KEY ("userDeletedId") REFERENCES "User"("userId") ON DELETE SET NULL ON UPDATE CASCADE;
7673

src/backend/src/prisma/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ model Material {
491491
materialId String @id @default(uuid())
492492
assembly Assembly? @relation(fields: [assemblyId], references: [assemblyId])
493493
assemblyId String?
494-
name String @unique
494+
name String
495495
wbsElement WBS_Element @relation(fields: [wbsElementId], references: [wbsElementId])
496496
wbsElementId Int
497497
dateDeleted DateTime?

src/backend/src/routes/projects.routes.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,32 @@ projectRouter.post(
7474
nonEmptyString(body('manufacturerPartNumber')),
7575
nonEmptyString(body('pdmFileName').optional()),
7676
intMinZero(body('quantity')),
77-
nonEmptyString(body('unitName')),
77+
nonEmptyString(body('unitName')).optional(),
7878
intMinZero(body('price')), // in cents
7979
intMinZero(body('subtotal')), // in cents
8080
nonEmptyString(body('linkUrl').isURL()),
8181
body('notes').isString(),
8282
validateInputs,
8383
ProjectsController.createMaterial
8484
);
85+
projectRouter.post(
86+
'/bom/material/:materialId/edit',
87+
nonEmptyString(body('name')),
88+
nonEmptyString(body('assemblyId').optional()),
89+
isMaterialStatus(body('status')),
90+
nonEmptyString(body('materialTypeName')),
91+
nonEmptyString(body('manufacturerName')),
92+
nonEmptyString(body('manufacturerPartNumber')),
93+
nonEmptyString(body('pdmFileName').optional()),
94+
intMinZero(body('quantity')),
95+
body('unitName').optional(),
96+
intMinZero(body('price')), // in cents
97+
intMinZero(body('subtotal')), // in cents
98+
nonEmptyString(body('linkUrl').isURL()),
99+
body('notes').isString(),
100+
validateInputs,
101+
ProjectsController.editMaterial
102+
);
85103

86104
projectRouter.delete('/bom/material-type/:materialTypeId/delete', ProjectsController.deleteMaterialType);
87105

src/backend/src/services/projects.services.ts

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -627,14 +627,14 @@ export default class ProjectsService {
627627
* @param manufacturerName the name of the material's manufacturer
628628
* @param manufacturerPartNumber the manufacturer part number for the material
629629
* @param quantity the quantity of material as a number
630-
* @param unitName the name of the Quantity Unit the quantity is measured in
631630
* @param price the price of the material in whole cents
632631
* @param subtotal the subtotal of the price for the material in whole cents
633632
* @param linkUrl the url for the material's link as a string
634633
* @param notes any notes about the material as a string
635634
* @param wbsNumber the WBS number of the project associated with this material
636635
* @param assemblyId the id of the Assembly for the material
637636
* @param pdmFileName the name of the pdm file for the material
637+
* @param unitName the name of the Quantity Unit the quantity is measured in
638638
* @returns the created material
639639
*/
640640
static async createMaterial(
@@ -645,14 +645,14 @@ export default class ProjectsService {
645645
manufacturerName: string,
646646
manufacturerPartNumber: string,
647647
quantity: number,
648-
unitName: string,
649648
price: number,
650649
subtotal: number,
651650
linkUrl: string,
652651
notes: string,
653652
wbsNumber: WbsNumber,
654653
assemblyId?: string,
655-
pdmFileName?: string
654+
pdmFileName?: string,
655+
unitName?: string
656656
): Promise<Material> {
657657
const project = await prisma.project.findFirst({
658658
where: {
@@ -682,10 +682,12 @@ export default class ProjectsService {
682682
});
683683
if (!manufacturer) throw new NotFoundException('Manufacturer', manufacturerName);
684684

685-
const unit = await prisma.unit.findFirst({
686-
where: { name: unitName }
687-
});
688-
if (!unit) throw new NotFoundException('Unit', unitName);
685+
if (unitName) {
686+
const unit = await prisma.unit.findFirst({
687+
where: { name: unitName }
688+
});
689+
if (!unit) throw new NotFoundException('Unit', unitName);
690+
}
689691

690692
const perms = isLeadership(creator.role) || isUserPartOfTeams(project.teams, creator);
691693

@@ -872,4 +874,109 @@ export default class ProjectsService {
872874

873875
return deletedMaterialType;
874876
}
877+
878+
/**
879+
* Update a material
880+
* @param submitter the submitter of the request
881+
* @param materialId the material id of the material being edited
882+
* @param name the name of the edited material
883+
* @param status the status of the edited material
884+
* @param materialTypeName the material type of the edited material
885+
* @param manufacturerName the manufacturerName of the edited material
886+
* @param manufacturerPartNumber the manufacturerPartNumber of the edited material
887+
* @param quantity the quantity of the edited material
888+
* @param price the price of the edited material
889+
* @param subtotal the subtotal of the edited material
890+
* @param linkUrl the linkUrl of the edited material
891+
* @param notes the notes of the edited material
892+
* @param unitName the unit name of the edited material
893+
* @param assemblyId the assembly id of the edited material
894+
* @param pdmFileName the pdm file name of the edited material
895+
* @throws if permission denied or material's wbsElement is undefined/deleted
896+
* @returns the updated material
897+
*/
898+
static async editMaterial(
899+
submitter: User,
900+
materialId: string,
901+
name: string,
902+
status: Material_Status,
903+
materialTypeName: string,
904+
manufacturerName: string,
905+
manufacturerPartNumber: string,
906+
quantity: number,
907+
price: number,
908+
subtotal: number,
909+
linkUrl: string,
910+
notes: string,
911+
unitName?: string,
912+
assemblyId?: string,
913+
pdmFileName?: string
914+
): Promise<Material> {
915+
const material = await prisma.material.findUnique({
916+
where: {
917+
materialId
918+
}
919+
});
920+
921+
if (!material) throw new NotFoundException('Material', materialId);
922+
if (material.dateDeleted) throw new DeletedException('Material', materialId);
923+
924+
const project = await prisma.project.findFirst({
925+
where: {
926+
wbsElementId: material.wbsElementId
927+
},
928+
...projectQueryArgs
929+
});
930+
931+
if (!project) throw new NotFoundException('Project', material.wbsElementId);
932+
if (project.wbsElement.dateDeleted) throw new DeletedException('Project', project.projectId);
933+
934+
if (assemblyId) {
935+
const assembly = await prisma.assembly.findFirst({ where: { assemblyId } });
936+
if (!assembly) throw new NotFoundException('Assembly', assemblyId);
937+
}
938+
939+
const materialType = await prisma.material_Type.findFirst({
940+
where: { name: materialTypeName }
941+
});
942+
if (!materialType) throw new NotFoundException('Material Type', materialTypeName);
943+
944+
const manufacturer = await prisma.manufacturer.findFirst({
945+
where: { name: manufacturerName }
946+
});
947+
if (!manufacturer) throw new NotFoundException('Manufacturer', manufacturerName);
948+
949+
if (unitName) {
950+
const unit = await prisma.unit.findFirst({
951+
where: { name: unitName }
952+
});
953+
if (!unit) throw new NotFoundException('Unit', unitName);
954+
}
955+
956+
const perms = isLeadership(submitter.role) || isUserPartOfTeams(project.teams, submitter);
957+
958+
if (!perms) throw new AccessDeniedException('update material');
959+
960+
const updatedMaterial = await prisma.material.update({
961+
where: { materialId },
962+
data: {
963+
name,
964+
status,
965+
materialTypeName,
966+
manufacturerName,
967+
manufacturerPartNumber,
968+
quantity,
969+
unitName,
970+
price,
971+
subtotal,
972+
linkUrl,
973+
notes,
974+
wbsElementId: project.wbsElementId,
975+
assemblyId,
976+
pdmFileName
977+
}
978+
});
979+
980+
return updatedMaterial;
981+
}
875982
}

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
removeDeletedReceiptPictures,
2525
updateReimbursementProducts,
2626
validateReimbursementProducts,
27+
validateUserEditRRPermissions,
2728
validateUserIsPartOfFinanceTeam
2829
} from '../utils/reimbursement-requests.utils';
2930
import {
@@ -255,10 +256,7 @@ export default class ReimbursementRequestService {
255256

256257
if (!oldReimbursementRequest) throw new NotFoundException('Reimbursement Request', requestId);
257258
if (oldReimbursementRequest.dateDeleted) throw new DeletedException('Reimbursement Request', requestId);
258-
if (oldReimbursementRequest.recipientId !== submitter.userId)
259-
throw new AccessDeniedException(
260-
'You do not have access to delete this reimbursement request, only the creator can edit a reimbursement request'
261-
);
259+
await validateUserEditRRPermissions(submitter, oldReimbursementRequest);
262260

263261
const vendor = await prisma.vendor.findUnique({
264262
where: { vendorId }

src/backend/src/utils/errors.utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,5 @@ type ExceptionObjectNames =
115115
| 'Assembly'
116116
| 'Material Type'
117117
| 'Manufacturer'
118-
| 'Unit';
118+
| 'Unit'
119+
| 'Material';

src/backend/src/utils/projects.utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,40 @@ export const getUserFullName = async (userId: number | null): Promise<string | n
6060
if (!user) throw new NotFoundException('User', userId);
6161
return `${user.firstName} ${user.lastName}`;
6262
};
63+
64+
/**
65+
* Check if given assembly, material type, manufacturer, and unit exist in the app database
66+
* @param manufacturerName the manufacure of the material to check if it exists
67+
* @param materialTypeName the material type of the material to check if it exists
68+
* @param unitName the unit of the material to check if it exists
69+
* @param assemblyId the assembly of the material to check if it exists
70+
* @throws if any of these properties of the material does not exist in the db
71+
*/
72+
export const checkMaterialInputs = async (
73+
manufacturerName: string,
74+
materialTypeName: string,
75+
unitName?: string,
76+
assemblyId?: string
77+
) => {
78+
if (assemblyId) {
79+
const assembly = await prisma.assembly.findFirst({ where: { assemblyId } });
80+
if (!assembly) throw new NotFoundException('Assembly', assemblyId);
81+
}
82+
83+
const materialType = await prisma.material_Type.findFirst({
84+
where: { name: materialTypeName }
85+
});
86+
if (!materialType) throw new NotFoundException('Material Type', materialTypeName);
87+
88+
const manufacturer = await prisma.manufacturer.findFirst({
89+
where: { name: manufacturerName }
90+
});
91+
if (!manufacturer) throw new NotFoundException('Manufacturer', manufacturerName);
92+
93+
if (unitName) {
94+
const unit = await prisma.unit.findFirst({
95+
where: { name: unitName }
96+
});
97+
if (!unit) throw new NotFoundException('Unit', unitName);
98+
}
99+
};

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { ReimbursementProductCreateArgs, ReimbursementReceiptCreateArgs, WbsNumber, wbsPipe } from 'shared';
77
import prisma from '../prisma/prisma';
88
import { AccessDeniedException, DeletedException, HttpException, NotFoundException } from './errors.utils';
9-
import { Prisma, Receipt, Reimbursement_Product, Team, User } from '@prisma/client';
9+
import { Prisma, Receipt, Reimbursement_Product, Reimbursement_Request, Team, User } from '@prisma/client';
1010
import authUserQueryArgs from '../prisma-query-args/auth-user.query-args';
1111
import { isUserOnTeam } from './teams.utils';
1212

@@ -233,3 +233,17 @@ export const isAuthUserHeadOfFinance = (user: Prisma.UserGetPayload<typeof authU
233233
const isTeamIdInList = (teamId: string, teamsList: Team[]) => {
234234
return teamsList.map((team) => team.teamId).includes(teamId);
235235
};
236+
237+
/**
238+
* Validates user has permission to edit the reimbursement request.
239+
* @param user the person editing the reimbursement request
240+
* @param reimbursementRequest the reimbursement request to edit
241+
*/
242+
export const validateUserEditRRPermissions = async (user: User, reimbursementRequest: Reimbursement_Request) => {
243+
try {
244+
await validateUserIsPartOfFinanceTeam(user);
245+
} catch {
246+
if (reimbursementRequest.recipientId !== user.userId)
247+
throw new AccessDeniedException('Only the creator or finance team can edit a reimbursement request');
248+
}
249+
};

0 commit comments

Comments
 (0)