Skip to content

Commit 172907a

Browse files
authored
Merge pull request #1651 from Northeastern-Electric-Racing/#1525-create-materials
#1525 Create Materials Endpoint
2 parents 6bfeece + 58b5f49 commit 172907a

8 files changed

Lines changed: 413 additions & 10 deletions

File tree

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,48 @@ export default class ProjectsController {
140140
}
141141
}
142142

143+
static async createMaterial(req: Request, res: Response, next: NextFunction) {
144+
try {
145+
const {
146+
name,
147+
assemblyId,
148+
status,
149+
materialTypeName,
150+
manufacturerName,
151+
manufacturerPartNumber,
152+
pdmFileName,
153+
quantity,
154+
unitName,
155+
price,
156+
subtotal,
157+
linkUrl,
158+
notes
159+
} = req.body;
160+
const creator = await getCurrentUser(res);
161+
const wbsNum = validateWBS(req.params.wbsNum);
162+
const material = await ProjectsService.createMaterial(
163+
creator,
164+
name,
165+
status,
166+
materialTypeName,
167+
manufacturerName,
168+
manufacturerPartNumber,
169+
quantity,
170+
unitName,
171+
price,
172+
subtotal,
173+
linkUrl,
174+
notes,
175+
wbsNum,
176+
assemblyId,
177+
pdmFileName
178+
);
179+
return res.status(200).json(material);
180+
} catch (error: unknown) {
181+
next(error);
182+
}
183+
}
184+
143185
static async createManufacturer(req: Request, res: Response, next: NextFunction) {
144186
try {
145187
const { name } = req.body;

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import express from 'express';
22
import { body } from 'express-validator';
3-
import { intMinZero, nonEmptyString } from '../utils/validation.utils';
3+
import { intMinZero, isMaterialStatus, nonEmptyString } from '../utils/validation.utils';
44
import { validateInputs } from '../utils/utils';
55
import ProjectsController from '../controllers/projects.controllers';
66

@@ -62,5 +62,23 @@ projectRouter.post(
6262
nonEmptyString(body('pdmFileName')).optional(),
6363
ProjectsController.createAssembly
6464
);
65+
projectRouter.post(
66+
'/material/:wbsNum/create',
67+
nonEmptyString(body('name')),
68+
nonEmptyString(body('assemblyId').optional()),
69+
isMaterialStatus(body('status')),
70+
nonEmptyString(body('materialTypeName')),
71+
nonEmptyString(body('manufacturerName')),
72+
nonEmptyString(body('manufacturerPartNumber')),
73+
nonEmptyString(body('pdmFileName').optional()),
74+
intMinZero(body('quantity')),
75+
nonEmptyString(body('unitName')),
76+
intMinZero(body('price')), // in cents
77+
intMinZero(body('subtotal')), // in cents
78+
nonEmptyString(body('linkUrl').isURL()),
79+
body('notes').isString(),
80+
validateInputs,
81+
ProjectsController.createMaterial
82+
);
6583

6684
export default projectRouter;

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

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Material_Type, User, Assembly } from '@prisma/client';
1+
import { Material_Type, User, Assembly, Material_Status, Material } from '@prisma/client';
22
import { isAdmin, isGuest, isLeadership, isProject, LinkCreateArgs, LinkType, Project, WbsNumber, wbsPipe } from 'shared';
33
import projectQueryArgs from '../prisma-query-args/projects.query-args';
44
import prisma from '../prisma/prisma';
@@ -605,6 +605,103 @@ export default class ProjectsService {
605605
).map(linkTypeTransformer);
606606
}
607607

608+
/**
609+
* Creates a new Material
610+
* @param creator the user creating the material
611+
* @param name the name of the material
612+
* @param status the Material Status of the material
613+
* @param materialTypeName the name of the Material Type
614+
* @param manufacturerName the name of the material's manufacturer
615+
* @param manufacturerPartNumber the manufacturer part number for the material
616+
* @param quantity the quantity of material as a number
617+
* @param unitName the name of the Quantity Unit the quantity is measured in
618+
* @param price the price of the material in whole cents
619+
* @param subtotal the subtotal of the price for the material in whole cents
620+
* @param linkUrl the url for the material's link as a string
621+
* @param notes any notes about the material as a string
622+
* @param wbsNumber the WBS number of the project associated with this material
623+
* @param assemblyId the id of the Assembly for the material
624+
* @param pdmFileName the name of the pdm file for the material
625+
* @returns the created material
626+
*/
627+
static async createMaterial(
628+
creator: User,
629+
name: string,
630+
status: Material_Status,
631+
materialTypeName: string,
632+
manufacturerName: string,
633+
manufacturerPartNumber: string,
634+
quantity: number,
635+
unitName: string,
636+
price: number,
637+
subtotal: number,
638+
linkUrl: string,
639+
notes: string,
640+
wbsNumber: WbsNumber,
641+
assemblyId?: string,
642+
pdmFileName?: string
643+
): Promise<Material> {
644+
const project = await prisma.project.findFirst({
645+
where: {
646+
wbsElement: {
647+
carNumber: wbsNumber.carNumber,
648+
projectNumber: wbsNumber.projectNumber,
649+
workPackageNumber: wbsNumber.workPackageNumber
650+
}
651+
},
652+
...projectQueryArgs
653+
});
654+
655+
if (!project) throw new NotFoundException('Project', wbsPipe(wbsNumber));
656+
657+
if (assemblyId) {
658+
const assembly = await prisma.assembly.findFirst({ where: { assemblyId } });
659+
if (!assembly) throw new NotFoundException('Assembly', assemblyId);
660+
}
661+
662+
const materialType = await prisma.material_Type.findFirst({
663+
where: { name: materialTypeName }
664+
});
665+
if (!materialType) throw new NotFoundException('Material Type', materialTypeName);
666+
667+
const manufacturer = await prisma.manufacturer.findFirst({
668+
where: { name: manufacturerName }
669+
});
670+
if (!manufacturer) throw new NotFoundException('Manufacturer', manufacturerName);
671+
672+
const unit = await prisma.unit.findFirst({
673+
where: { name: unitName }
674+
});
675+
if (!unit) throw new NotFoundException('Unit', unitName);
676+
677+
const perms = isLeadership(creator.role) || isUserPartOfTeams(project.teams, creator);
678+
679+
if (!perms) throw new AccessDeniedException('create materials');
680+
681+
const createdMaterial = await prisma.material.create({
682+
data: {
683+
userCreatedId: creator.userId,
684+
name,
685+
assemblyId,
686+
status,
687+
materialTypeName,
688+
manufacturerName,
689+
manufacturerPartNumber,
690+
pdmFileName,
691+
quantity,
692+
unitName,
693+
price,
694+
subtotal,
695+
linkUrl,
696+
notes,
697+
dateCreated: new Date(),
698+
wbsElementId: project.wbsElementId
699+
}
700+
});
701+
702+
return createdMaterial;
703+
}
704+
608705
/**
609706
* Create an assembly
610707
* @param name The name of the assembly to be created

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,8 @@ type ExceptionObjectNames =
111111
| 'Expense Type'
112112
| 'Reimbursement Request'
113113
| 'User Secure Settings'
114-
| 'Image File';
114+
| 'Image File'
115+
| 'Assembly'
116+
| 'Material Type'
117+
| 'Manufacturer'
118+
| 'Unit';

src/backend/src/utils/validation.utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ValidationChain } from 'express-validator';
2-
import { ClubAccount } from 'shared';
2+
import { ClubAccount, MaterialStatus } from 'shared';
33
import { TaskPriority, TaskStatus, WorkPackageStage, RoleEnum } from 'shared';
44

55
export const intMinZero = (validationObject: ValidationChain): ValidationChain => {
@@ -44,3 +44,9 @@ export const isWorkPackageStageOrNone = (validationObject: ValidationChain): Val
4444
export const isAccount = (validationObject: ValidationChain): ValidationChain => {
4545
return validationObject.isString().isIn([ClubAccount.BUDGET, ClubAccount.CASH]);
4646
};
47+
48+
export const isMaterialStatus = (validationObject: ValidationChain): ValidationChain => {
49+
return validationObject
50+
.isString()
51+
.isIn([MaterialStatus.Ordered, MaterialStatus.Received, MaterialStatus.Unordered, MaterialStatus.Shipped]);
52+
};

0 commit comments

Comments
 (0)