Skip to content

Commit ae7d323

Browse files
committed
#1610 Added deny reimbursement request endpoint
1 parent 3c37f8f commit ae7d323

8 files changed

Lines changed: 117 additions & 2 deletions

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;

src/backend/src/prisma/migrations/20230629193452_finance/migration.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
CREATE TYPE "Club_Accounts" AS ENUM ('CASH', 'BUDGET');
33

44
-- CreateEnum
5-
CREATE TYPE "Reimbursement_Status_Type" AS ENUM ('PENDING_FINANCE', 'SABO_SUBMITTED', 'ADVISOR_APPROVED', 'REIMBURSED');
5+
CREATE TYPE "Reimbursement_Status_Type" AS ENUM ('PENDING_FINANCE', 'SABO_SUBMITTED', 'ADVISOR_APPROVED', 'REIMBURSED', 'DENIED');
66

77
-- CreateTable
88
CREATE TABLE "Reimbursement_Status" (

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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,49 @@ 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 approving 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 (
727+
reimbursementRequest.reimbursementStatuses.some((status) => status.type === ReimbursementStatusType.SABO_SUBMITTED)
728+
) {
729+
throw new HttpException(400, 'This reimbursement request has already been approved');
730+
}
731+
732+
const reimbursementStatus = await prisma.reimbursement_Status.create({
733+
data: {
734+
type: ReimbursementStatusType.DENIED,
735+
userId: submitter.userId,
736+
reimbursementRequestId: reimbursementRequest.reimbursementRequestId
737+
},
738+
include: {
739+
user: true
740+
}
741+
});
742+
743+
return reimbursementStatusTransformer(reimbursementStatus);
744+
}
745+
703746
/**
704747
* Downloads the receipt image file with the given google file id
705748
*

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

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,61 @@ describe('Reimbursement Requests', () => {
574574
});
575575
});
576576

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

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)