Skip to content

Commit 06abc2d

Browse files
authored
Merge pull request #1686 from Northeastern-Electric-Racing/#1610-Deny-Reimbursement-Request-Endpoint
#1610 Added deny reimbursement request endpoint
2 parents 56aa6d8 + d2f495e commit 06abc2d

9 files changed

Lines changed: 168 additions & 1 deletion

File tree

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,17 @@ export default class ReimbursementRequestsController {
230230
}
231231
}
232232

233+
static async denyReimbursementRequest(req: Request, res: Response, next: NextFunction) {
234+
try {
235+
const { requestId } = req.params;
236+
const user = await getCurrentUser(res);
237+
const reimbursementStatus = await ReimbursementRequestService.denyReimbursementRequest(requestId, user);
238+
res.status(200).json(reimbursementStatus);
239+
} catch (error: unknown) {
240+
next(error);
241+
}
242+
}
243+
233244
static async markReimbursementRequestAsDelivered(req: Request, res: Response, next: NextFunction) {
234245
try {
235246
const { requestId } = req.params;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterEnum
2+
ALTER TYPE "Reimbursement_Status_Type" ADD VALUE 'DENIED';

src/backend/src/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ enum Reimbursement_Status_Type {
364364
SABO_SUBMITTED
365365
ADVISOR_APPROVED
366366
REIMBURSED
367+
DENIED
367368
}
368369

369370
model Reimbursement_Status {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ reimbursementRequestsRouter.post(
128128

129129
reimbursementRequestsRouter.post('/:requestId/approve', ReimbursementRequestController.approveReimbursementRequest);
130130
reimbursementRequestsRouter.delete('/:requestId/delete', ReimbursementRequestController.deleteReimbursementRequest);
131+
reimbursementRequestsRouter.post('/:requestId/deny', ReimbursementRequestController.denyReimbursementRequest);
131132

132133
reimbursementRequestsRouter.post(
133134
'/:requestId/delivered',

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,51 @@ export default class ReimbursementRequestService {
700700
return reimbursementStatusTransformer(reimbursementStatus);
701701
}
702702

703+
/**
704+
* Adds a reimbursement status with type denied to the given reimbursement request
705+
*
706+
* @param reimbursementRequestId the id of the reimbursement request to deny
707+
* @param submitter the user who is denying the reimbursement request
708+
* @returns the created reimbursment status
709+
*/
710+
static async denyReimbursementRequest(reimbursementRequestId: string, submitter: User) {
711+
await validateUserIsPartOfFinanceTeam(submitter);
712+
713+
const reimbursementRequest = await prisma.reimbursement_Request.findUnique({
714+
where: { reimbursementRequestId },
715+
include: {
716+
reimbursementStatuses: true
717+
}
718+
});
719+
720+
if (!reimbursementRequest) throw new NotFoundException('Reimbursement Request', reimbursementRequestId);
721+
722+
if (reimbursementRequest.dateDeleted) {
723+
throw new DeletedException('Reimbursement Request', reimbursementRequestId);
724+
}
725+
726+
if (reimbursementRequest.reimbursementStatuses.some((status) => status.type === ReimbursementStatusType.DENIED)) {
727+
throw new HttpException(400, 'This reimbursement request has already been denied');
728+
}
729+
730+
if (reimbursementRequest.reimbursementStatuses.some((status) => status.type === ReimbursementStatusType.REIMBURSED)) {
731+
throw new HttpException(400, 'This reimbursement request has already been reimbursed');
732+
}
733+
734+
const reimbursementStatus = await prisma.reimbursement_Status.create({
735+
data: {
736+
type: ReimbursementStatusType.DENIED,
737+
userId: submitter.userId,
738+
reimbursementRequestId: reimbursementRequest.reimbursementRequestId
739+
},
740+
include: {
741+
user: true
742+
}
743+
});
744+
745+
return reimbursementStatusTransformer(reimbursementStatus);
746+
}
747+
703748
/**
704749
* Downloads the receipt image file with the given google file id
705750
*

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
prismaGiveMeMyMoney,
2020
prismaGiveMeMyMoney2,
2121
prismaGiveMeMyMoney3,
22+
prismaGiveMeMyMoney4,
23+
prismaGiveMeMyMoney5,
2224
prismaReimbursementStatus,
2325
sharedGiveMeMyMoney
2426
} from './test-data/reimbursement-requests.test-data';
@@ -574,6 +576,69 @@ describe('Reimbursement Requests', () => {
574576
});
575577
});
576578

579+
describe('Deny Reimbursement Request Tests', () => {
580+
test('Deny Reimbursement Request fails if Submitter not on Finance Team', async () => {
581+
vi.spyOn(prisma.team, 'findUnique').mockResolvedValue({ ...primsaTeam2, headId: 1 });
582+
await expect(
583+
ReimbursementRequestService.denyReimbursementRequest(GiveMeMyMoney.reimbursementRequestId, alfred)
584+
).rejects.toThrow(new AccessDeniedException(`You are not a member of the finance team!`));
585+
});
586+
587+
test('Deny Reimbursement Request fails if Finance Team does not exist', async () => {
588+
vi.spyOn(prisma.team, 'findUnique').mockResolvedValue(null);
589+
await expect(
590+
ReimbursementRequestService.denyReimbursementRequest(GiveMeMyMoney.reimbursementRequestId, alfred)
591+
).rejects.toThrow(new HttpException(500, 'Finance team does not exist!'));
592+
});
593+
594+
test('Deny Reimbursement Request fails if the Request does not exist', async () => {
595+
vi.spyOn(prisma.team, 'findUnique').mockResolvedValue(primsaTeam2);
596+
vi.spyOn(prisma.reimbursement_Request, 'findUnique').mockResolvedValue(null);
597+
598+
await expect(
599+
ReimbursementRequestService.denyReimbursementRequest(GiveMeMyMoney.reimbursementRequestId, alfred)
600+
).rejects.toThrow(new NotFoundException('Reimbursement Request', GiveMeMyMoney.reimbursementRequestId));
601+
});
602+
603+
test('Deny Reimbursement Request fails if the Request has been deleted', async () => {
604+
vi.spyOn(prisma.team, 'findUnique').mockResolvedValue(primsaTeam2);
605+
vi.spyOn(prisma.reimbursement_Request, 'findUnique').mockResolvedValue(GiveMeMyMoney2);
606+
607+
await expect(
608+
ReimbursementRequestService.denyReimbursementRequest(GiveMeMyMoney2.reimbursementRequestId, alfred)
609+
).rejects.toThrow(new DeletedException('Reimbursement Request', GiveMeMyMoney2.reimbursementRequestId));
610+
});
611+
612+
test('Deny Reimbursement Request fails if the request has already been denied', async () => {
613+
vi.spyOn(prisma.team, 'findUnique').mockResolvedValue(primsaTeam2);
614+
vi.spyOn(prisma.reimbursement_Request, 'findUnique').mockResolvedValue(prismaGiveMeMyMoney4);
615+
616+
await expect(
617+
ReimbursementRequestService.denyReimbursementRequest(prismaGiveMeMyMoney4.reimbursementRequestId, alfred)
618+
).rejects.toThrow(new HttpException(400, 'This reimbursement request has already been denied'));
619+
});
620+
test('Deny Reimbursement Request fails if the request has already been reimbursed', async () => {
621+
vi.spyOn(prisma.team, 'findUnique').mockResolvedValue(primsaTeam2);
622+
vi.spyOn(prisma.reimbursement_Request, 'findUnique').mockResolvedValue(prismaGiveMeMyMoney5);
623+
624+
await expect(
625+
ReimbursementRequestService.denyReimbursementRequest(prismaGiveMeMyMoney5.reimbursementRequestId, alfred)
626+
).rejects.toThrow(new HttpException(400, 'This reimbursement request has already been reimbursed'));
627+
});
628+
test('Deny Reimbursement Request success', async () => {
629+
vi.spyOn(prisma.team, 'findUnique').mockResolvedValue(primsaTeam2);
630+
vi.spyOn(prisma.reimbursement_Request, 'findUnique').mockResolvedValue(prismaGiveMeMyMoney3);
631+
vi.spyOn(prisma.reimbursement_Status, 'create').mockResolvedValue(prismaReimbursementStatus);
632+
633+
const reimbursementStatus = await ReimbursementRequestService.denyReimbursementRequest(
634+
prismaGiveMeMyMoney3.reimbursementRequestId,
635+
alfred
636+
);
637+
638+
expect(reimbursementStatus.reimbursementStatusId).toStrictEqual(prismaReimbursementStatus.reimbursementStatusId);
639+
});
640+
});
641+
577642
describe('Reimbursement User Tests', () => {
578643
test('Throws an error if user is a guest', async () => {
579644
await expect(ReimbursementRequestService.reimburseUser(100, '2023-01-11T11:12:33.409Z', theVisitor)).rejects.toThrow(

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,24 @@ export const prismaReimbursementStatus2: PrismaReimbursementStatus & { user: Use
111111
user: alfred
112112
};
113113

114+
export const prismaReimbursementStatus3: PrismaReimbursementStatus & { user: User } = {
115+
reimbursementStatusId: 3,
116+
type: 'DENIED',
117+
userId: 0,
118+
dateCreated: new Date('23/8/2023'),
119+
reimbursementRequestId: GiveMeMyMoney.reimbursementRequestId,
120+
user: alfred
121+
};
122+
123+
export const prismaReimbursementStatus4: PrismaReimbursementStatus & { user: User } = {
124+
reimbursementStatusId: 3,
125+
type: 'REIMBURSED',
126+
userId: 0,
127+
dateCreated: new Date('23/8/2023'),
128+
reimbursementRequestId: GiveMeMyMoney.reimbursementRequestId,
129+
user: alfred
130+
};
131+
114132
export const prismaGiveMeMyMoney2: Prisma.Reimbursement_RequestGetPayload<typeof reimbursementRequestQueryArgs> = {
115133
...GiveMeMyMoney,
116134
receiptPictures: [],
@@ -131,6 +149,26 @@ export const prismaGiveMeMyMoney3: Prisma.Reimbursement_RequestGetPayload<typeof
131149
expenseType: Parts
132150
};
133151

152+
export const prismaGiveMeMyMoney4: Prisma.Reimbursement_RequestGetPayload<typeof reimbursementRequestQueryArgs> = {
153+
...GiveMeMyMoney,
154+
receiptPictures: [],
155+
reimbursementStatuses: [prismaReimbursementStatus3],
156+
recipient: batman,
157+
vendor: PopEyes,
158+
reimbursementProducts: [{ ...GiveMeMoneyProduct, wbsElement: prismaWbsElement1 }],
159+
expenseType: Parts
160+
};
161+
162+
export const prismaGiveMeMyMoney5: Prisma.Reimbursement_RequestGetPayload<typeof reimbursementRequestQueryArgs> = {
163+
...GiveMeMyMoney,
164+
receiptPictures: [],
165+
reimbursementStatuses: [prismaReimbursementStatus4],
166+
recipient: batman,
167+
vendor: PopEyes,
168+
reimbursementProducts: [{ ...GiveMeMoneyProduct, wbsElement: prismaWbsElement1 }],
169+
expenseType: Parts
170+
};
171+
134172
export const prismaGiveMeMyMoney3Approved: Prisma.Reimbursement_RequestGetPayload<typeof reimbursementRequestQueryArgs> = {
135173
...GiveMeMyMoney,
136174
receiptPictures: [],

src/frontend/src/utils/reimbursement-request.utils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ export const cleanReimbursementRequestStatus = (status: ReimbursementStatusType)
5959
case ReimbursementStatusType.SABO_SUBMITTED: {
6060
return 'Submitted to Sabo';
6161
}
62+
case ReimbursementStatusType.DENIED: {
63+
return 'Denied';
64+
}
6265
}
6366
};
6467

src/shared/src/types/reimbursement-requests-types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ export enum ReimbursementStatusType {
1010
PENDING_FINANCE = 'PENDING_FINANCE',
1111
SABO_SUBMITTED = 'SABO_SUBMITTED',
1212
ADVISOR_APPROVED = 'ADVISOR_APPROVED',
13-
REIMBURSED = 'REIMBURSED'
13+
REIMBURSED = 'REIMBURSED',
14+
DENIED = 'DENIED'
1415
}
1516

1617
export interface ReimbursementStatus {

0 commit comments

Comments
 (0)