Skip to content

Commit ca12916

Browse files
committed
updated validation, id handling and added tests
1 parent 4d389c7 commit ca12916

3 files changed

Lines changed: 125 additions & 11 deletions

File tree

src/backend/src/services/boms.services.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,24 +181,21 @@ export default class BillOfMaterialsService {
181181
...getMaterialQueryArgs(organization.organizationId)
182182
});
183183

184-
// Validate all materials were found
185184
if (materials.length !== materialIds.length) throw new NotFoundException('Material', 'Not all materials found');
186185

187-
// Validate all materials are from the current organization
188-
const invalidMaterials = materials.filter((material) => material.unit?.organizationId !== organization.organizationId);
186+
const invalidMaterials = materials.filter(
187+
(material) => material.materialType.organizationId !== organization.organizationId
188+
);
189189
if (invalidMaterials.length > 0) throw new HttpException(400, 'All materials must be from the current organization');
190190

191-
// Fetch destination project (validates project is in the user's organization)
192191
const destinationProject = await ProjectsService.getSingleProjectWithQueryArgs(destinationProjectId, organization);
193192

194-
// Check user has correct permissions
195193
const perms =
196194
(await userHasPermission(user.userId, organization.organizationId, isLeadership)) ||
197195
isUserPartOfTeams(destinationProject.teams, user);
198196

199197
if (!perms) throw new AccessDeniedException('Permission to copy materials denied');
200198

201-
// Create copied materials (all or none)
202199
return await prisma.$transaction(async (tx) => {
203200
const newMaterialIds: string[] = [];
204201

@@ -224,7 +221,6 @@ export default class BillOfMaterialsService {
224221
if (!unit) throw new NotFoundException('Unit', material.unitId);
225222
}
226223

227-
// Create the new material
228224
const newMaterial = await tx.material.create({
229225
data: {
230226
name: material.name,

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,10 +212,14 @@ export const materialValidators = [
212212
export const copyMaterialsValidators = [
213213
body('materialIds').isArray({ min: 1 }),
214214
body('materialIds.*').isString().notEmpty(),
215-
body('destinationWbsNum').custom((value) => {
216-
validateWBS(value);
217-
return true;
218-
})
215+
body('destinationWbsNum')
216+
.custom((value) => {
217+
validateWBS(value);
218+
return true;
219+
})
220+
.customSanitizer((value) => {
221+
return validateWBS(value);
222+
})
219223
];
220224
export const validateInputs = (req: Request, res: Response, next: Function): void => {
221225
const errors = validationResult(req);

src/backend/tests/unmocked/project.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import prisma from '../../src/prisma/prisma.js';
12
import { createTestReimbursementRequest, resetUsers } from '../test-utils.js';
23
import { Organization, User } from '@prisma/client';
34
import BillOfMaterials from '../../src/services/boms.services.js';
@@ -93,6 +94,119 @@ describe('Material Tests', () => {
9394
});
9495
});
9596

97+
describe('Copy materials to project', () => {
98+
test('Successfully copies materials and resets key fields', async () => {
99+
const materialType = await BillOfMaterials.createMaterialType('Capacitor', createdUser, org);
100+
const manufacturer = await BillOfMaterials.createManufacturer(createdUser, 'Mouser', org);
101+
102+
const car = await prisma.car.findFirst({
103+
where: {
104+
wbsElement: {
105+
organizationId: org.organizationId
106+
}
107+
}
108+
});
109+
110+
const project2 = await prisma.project.create({
111+
data: {
112+
summary: 'Test destination project',
113+
car: {
114+
connect: { carId: car!.carId }
115+
},
116+
wbsElement: {
117+
create: {
118+
carNumber: 0,
119+
projectNumber: 2,
120+
workPackageNumber: 0,
121+
name: 'Destination Project',
122+
organizationId: org.organizationId
123+
}
124+
}
125+
}
126+
});
127+
128+
const material1 = await BillOfMaterials.createMaterial(
129+
createdUser,
130+
'100uF Capacitor',
131+
MaterialStatus.Ordered,
132+
materialType.name,
133+
'https://example.com/mat1',
134+
{ carNumber: 0, projectNumber: 1, workPackageNumber: 0 },
135+
org,
136+
manufacturer.name,
137+
'CAP-100UF',
138+
new Decimal(10),
139+
50,
140+
500,
141+
'Test notes',
142+
undefined,
143+
undefined,
144+
undefined,
145+
reimbursementRequest.reimbursementRequestId
146+
);
147+
148+
const material2 = await BillOfMaterials.createMaterial(
149+
createdUser,
150+
'220uF Capacitor',
151+
MaterialStatus.ReadyToOrder,
152+
materialType.name,
153+
'https://example.com/mat2',
154+
{ carNumber: 0, projectNumber: 1, workPackageNumber: 0 },
155+
org,
156+
manufacturer.name,
157+
'CAP-220UF',
158+
new Decimal(5),
159+
75,
160+
375
161+
);
162+
163+
const newMaterialIds = await BillOfMaterials.copyMaterialsToProject(
164+
createdUser,
165+
[material1.materialId, material2.materialId],
166+
{ carNumber: 0, projectNumber: 2, workPackageNumber: 0 },
167+
org
168+
);
169+
170+
expect(newMaterialIds.length).toBe(2);
171+
172+
const copiedMaterials = await prisma.material.findMany({
173+
where: { materialId: { in: newMaterialIds } },
174+
include: { wbsElement: true }
175+
});
176+
177+
const copiedMat1 = copiedMaterials.find((m) => m.name === '100uF Capacitor')!;
178+
const copiedMat2 = copiedMaterials.find((m) => m.name === '220uF Capacitor')!;
179+
180+
expect(copiedMat1.wbsElement.projectNumber).toBe(2);
181+
expect(copiedMat2.wbsElement.projectNumber).toBe(2);
182+
183+
expect(copiedMat1.status).toBe('NOT_READY_TO_ORDER');
184+
expect(copiedMat1.userCreatedId).toBe(createdUser.userId);
185+
expect(copiedMat1.reimbursementRequestId).toBeNull();
186+
expect(copiedMat1.assemblyId).toBeNull();
187+
188+
expect(copiedMat1.name).toBe('100uF Capacitor');
189+
expect(copiedMat1.price).toBe(50);
190+
expect(copiedMat1.quantity?.toString()).toBe('10');
191+
expect(copiedMat1.manufacturerPartNumber).toBe('CAP-100UF');
192+
expect(copiedMat1.notes).toBe('Test notes');
193+
194+
expect(copiedMat2.status).toBe('NOT_READY_TO_ORDER');
195+
expect(copiedMat2.reimbursementRequestId).toBeNull();
196+
});
197+
198+
test('Fails when material does not exist', async () => {
199+
await expect(
200+
BillOfMaterials.copyMaterialsToProject(
201+
createdUser,
202+
['non-existent-id'],
203+
{ carNumber: 0, projectNumber: 1, workPackageNumber: 0 },
204+
org
205+
)
206+
).rejects.toThrow(NotFoundException);
207+
});
208+
});
209+
96210
describe('Edit a material', () => {
97211
test('Updates the reimbursement request when originally undefined', async () => {
98212
const materialType = await BillOfMaterials.createMaterialType('Resistor', createdUser, org);

0 commit comments

Comments
 (0)