Skip to content

Commit 8321338

Browse files
authored
Merge pull request #3528 from Northeastern-Electric-Racing/createSponsorTier
implemented everything to do with sponsor tiers
2 parents 4a1c0ea + 33b2506 commit 8321338

18 files changed

Lines changed: 621 additions & 25 deletions

File tree

src/backend/src/controllers/finance.controllers.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,15 @@ export default class FinanceController {
100100

101101
static async createSponsorTier(req: Request, res: Response, next: NextFunction) {
102102
try {
103-
const { name, colorHexCode } = req.body;
103+
const { name, colorHexCode, minSupportValue } = req.body;
104104

105-
const sponsor = await FinanceServices.createSponsorTier(req.currentUser, name, req.organization, colorHexCode);
105+
const sponsor = await FinanceServices.createSponsorTier(
106+
req.currentUser,
107+
name,
108+
req.organization,
109+
colorHexCode,
110+
minSupportValue
111+
);
106112
res.status(200).json(sponsor);
107113
} catch (error: unknown) {
108114
next(error);
@@ -324,4 +330,33 @@ export default class FinanceController {
324330
next(error);
325331
}
326332
}
333+
334+
static async deleteSponsorTier(req: Request, res: Response, next: NextFunction) {
335+
try {
336+
const { sponsorTierId } = req.params;
337+
const deletedSponsorTier = await FinanceServices.deleteSponsorTier(sponsorTierId, req.currentUser, req.organization);
338+
res.status(200).json(deletedSponsorTier);
339+
} catch (error: unknown) {
340+
next(error);
341+
}
342+
}
343+
344+
static async editSponsorTier(req: Request, res: Response, next: NextFunction) {
345+
try {
346+
const { sponsorTierId } = req.params;
347+
const { name, colorHexCode, minSupportValue } = req.body;
348+
349+
const updatedSponsorTier = await FinanceServices.editSponsorTier(
350+
req.currentUser,
351+
req.organization,
352+
sponsorTierId,
353+
name,
354+
colorHexCode,
355+
minSupportValue
356+
);
357+
res.status(200).json(updatedSponsorTier);
358+
} catch (error: unknown) {
359+
next(error);
360+
}
361+
}
327362
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- AlterTable
2+
ALTER TABLE "public"."Sponsor_Tier"
3+
ADD COLUMN "dateDeleted" TIMESTAMP(3),
4+
ADD COLUMN "deleterId" TEXT,
5+
ADD COLUMN "minSupportValue" INTEGER NOT NULL DEFAULT 0;
6+
7+
-- AlterTable
8+
ALTER TABLE "public"."Vendor" ALTER COLUMN "addedByUserId" DROP DEFAULT;
9+
10+
-- AddForeignKey
11+
ALTER TABLE "public"."Sponsor_Tier" ADD CONSTRAINT "Sponsor_Tier_deleterId_fkey" FOREIGN KEY ("deleterId") REFERENCES "public"."User"("userId") ON DELETE SET NULL ON UPDATE CASCADE;

src/backend/src/prisma/schema.prisma

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ model User {
256256
deletedOtherReimbursementProductReasons Reimbursement_Product_Other_Reason[] @relation(name: "deletedOtherReimbursementProductReasons")
257257
reimbursementRequestComments Reimbursement_Request_Comment[] @relation(name: "createdReimbursementRequestComments")
258258
deletedReimbursementRequestComments Reimbursement_Request_Comment[] @relation(name: "deletedReimbursementRequestComments")
259+
deletedSponsorTiers Sponsor_Tier[]
259260
}
260261

261262
model Role {
@@ -1345,12 +1346,16 @@ model Part_Review_Common_Mistake {
13451346
}
13461347

13471348
model Sponsor_Tier {
1348-
sponsorTierId String @id @default(uuid())
1349-
organization Organization @relation(fields: [organizationId], references: [organizationId])
1350-
organizationId String
1351-
name String
1352-
colorHexCode String
1353-
sponsors Sponsor[]
1349+
sponsorTierId String @id @default(uuid())
1350+
organization Organization @relation(fields: [organizationId], references: [organizationId])
1351+
organizationId String
1352+
name String
1353+
colorHexCode String
1354+
sponsors Sponsor[]
1355+
dateDeleted DateTime?
1356+
deleter User? @relation(fields: [deleterId], references: [userId])
1357+
deleterId String?
1358+
minSupportValue Int @default(0)
13541359
}
13551360

13561361
model Index_Code {

src/backend/src/prisma/seed.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3031,9 +3031,9 @@ const performSeed: () => Promise<void> = async () => {
30313031
}
30323032
});
30333033

3034-
const goldSponsorTier = await FinanceServices.createSponsorTier(thomasEmrax, 'Gold', ner, '#9F9156');
3035-
await FinanceServices.createSponsorTier(thomasEmrax, 'Silver', ner, '#C0C0C0');
3036-
await FinanceServices.createSponsorTier(thomasEmrax, 'Bronze', ner, '#CD7F32');
3034+
const goldSponsorTier = await FinanceServices.createSponsorTier(thomasEmrax, 'Gold', ner, '#9F9156', 3000);
3035+
await FinanceServices.createSponsorTier(thomasEmrax, 'Silver', ner, '#C0C0C0', 200);
3036+
await FinanceServices.createSponsorTier(thomasEmrax, 'Bronze', ner, '#CD7F32', 10);
30373037

30383038
const sponsor = await FinanceServices.createSponsor(
30393039
thomasEmrax,
@@ -3050,8 +3050,6 @@ const performSeed: () => Promise<void> = async () => {
30503050
'googlecode'
30513051
);
30523052

3053-
await FinanceServices.createSponsorTier(thomasEmrax, 'Silver', ner, 'C0C0C0');
3054-
30553053
await FinanceServices.createSponsorTask(
30563054
thomasEmrax,
30573055
ner,

src/backend/src/routes/finance.routes.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,15 @@ financeRouter.post(
145145
FinanceController.editSponsor
146146
);
147147

148+
financeRouter.delete('/sponsorTier/:sponsorTierId', FinanceController.deleteSponsorTier);
149+
150+
financeRouter.post(
151+
'/sponsorTier/:sponsorTierId/edit',
152+
nonEmptyString(body('name')),
153+
nonEmptyString(body('colorHexCode')),
154+
intMinZero(body('minSupportValue')),
155+
validateInputs,
156+
FinanceController.editSponsorTier
157+
);
158+
148159
export default financeRouter;

src/backend/src/services/finance.services.ts

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
CreateSponsorTask,
3+
isAdmin,
34
isHead,
45
ReimbursementRequestData,
56
SpendingBarData,
@@ -15,6 +16,7 @@ import {
1516
getSponsorTierQueryArgs
1617
} from '../prisma-query-args/sponsor.query.args';
1718
import {
19+
AccessDeniedAdminOnlyException,
1820
AccessDeniedException,
1921
DeletedException,
2022
HttpException,
@@ -176,17 +178,25 @@ export default class FinanceServices {
176178
* @param name tier name
177179
* @param organization current organization of the current user
178180
* @param colorHexCode tier color
181+
* @param minSupportValue minimum support value for the tier
179182
* @returns newly created sponsor tier
180183
*/
181-
static async createSponsorTier(submitter: User, name: string, organization: Organization, colorHexCode: string) {
184+
static async createSponsorTier(
185+
submitter: User,
186+
name: string,
187+
organization: Organization,
188+
colorHexCode: string,
189+
minSupportValue: number
190+
): Promise<SponsorTier> {
182191
if (!(await userHasPermission(submitter.userId, organization.organizationId, isHead)))
183192
throw new AccessDeniedException('Only heads can create a sponsor tier');
184193

185194
const sponsorTier = await prisma.sponsor_Tier.create({
186195
data: {
187196
name,
188197
organizationId: organization.organizationId,
189-
colorHexCode
198+
colorHexCode,
199+
minSupportValue
190200
},
191201
include: {
192202
organization: true
@@ -587,10 +597,97 @@ export default class FinanceServices {
587597
*/
588598
static async getAllSponsorTiers(organization: Organization): Promise<SponsorTier[]> {
589599
const allSponsorTiers = await prisma.sponsor_Tier.findMany({
590-
where: { organizationId: organization.organizationId },
600+
where: { organizationId: organization.organizationId, dateDeleted: null },
601+
orderBy: { minSupportValue: 'asc' },
591602
...getSponsorTierQueryArgs(organization.organizationId)
592603
});
593604

594605
return allSponsorTiers;
595606
}
607+
608+
/**
609+
* Soft deletes a given sponsor tier
610+
* @param sponsorTierId the id of the sponsor tier that is getting deleted
611+
* @param deleter the person deleting the sponsor tier
612+
* @param organization the organization the person deleting belongs to
613+
* @returns the deleted sponsor tier
614+
*/
615+
static async deleteSponsorTier(sponsorTierId: string, deleter: User, organization: Organization): Promise<SponsorTier> {
616+
const sponsorTier = await prisma.sponsor_Tier.findUnique({
617+
where: { sponsorTierId }
618+
});
619+
620+
if (!(await userHasPermission(deleter.userId, organization.organizationId, isAdmin))) {
621+
throw new AccessDeniedAdminOnlyException('delete a sponsor tier');
622+
}
623+
624+
if (!sponsorTier) throw new NotFoundException('Sponsor Tier', sponsorTierId);
625+
if (sponsorTier.organizationId !== organization.organizationId) throw new InvalidOrganizationException('Sponsor Tier');
626+
if (sponsorTier.dateDeleted) throw new DeletedException('Sponsor Tier', sponsorTierId);
627+
628+
const associatedSponsors = await prisma.sponsor.count({
629+
where: { sponsorTierId: sponsorTier.sponsorTierId, dateDeleted: null }
630+
});
631+
632+
if (associatedSponsors > 0) {
633+
throw new HttpException(
634+
400,
635+
`Cannot delete Sponsor Tier "${sponsorTier.name}" because it is associated with existing sponsors.`
636+
);
637+
}
638+
639+
const deletedSponsorTier = await prisma.sponsor_Tier.update({
640+
where: { sponsorTierId },
641+
data: { dateDeleted: new Date(), deleter: { connect: { userId: deleter.userId } } },
642+
...getSponsorTierQueryArgs(organization.organizationId)
643+
});
644+
645+
return deletedSponsorTier;
646+
}
647+
648+
/**
649+
* Edits a sponsor tier.
650+
* @param submitter current user editing the sponsor tier
651+
* @param organization current organization of the current user
652+
* @param sponsorTierId id of the sponsor tier to be edited
653+
* @param name updated tier name
654+
* @param colorHexCode updated tier color
655+
* @param minSupportValue updated minimum support value for the tier
656+
* @returns the updated sponsor tier
657+
* @throws AccessDeniedAdminOnlyException if the user lacks permissions.
658+
* @throws NotFoundException if the sponsor tier is not found.
659+
*/
660+
static async editSponsorTier(
661+
submitter: User,
662+
organization: Organization,
663+
sponsorTierId: string,
664+
name: string,
665+
colorHexCode: string,
666+
minSupportValue: number
667+
): Promise<SponsorTier> {
668+
if (!(await userHasPermission(submitter.userId, organization.organizationId, isAdmin)))
669+
throw new AccessDeniedAdminOnlyException('edit a sponsor tier');
670+
671+
const oldSponsorTier = await prisma.sponsor_Tier.findUnique({
672+
where: {
673+
sponsorTierId,
674+
organizationId: organization.organizationId
675+
}
676+
});
677+
678+
if (!oldSponsorTier) throw new NotFoundException('Sponsor Tier', sponsorTierId);
679+
if (oldSponsorTier.dateDeleted) throw new DeletedException('Sponsor Tier', sponsorTierId);
680+
681+
const updatedSponsorTier = await prisma.sponsor_Tier.update({
682+
where: { sponsorTierId: oldSponsorTier.sponsorTierId },
683+
data: {
684+
name,
685+
colorHexCode,
686+
minSupportValue
687+
},
688+
...getSponsorTierQueryArgs(organization.organizationId)
689+
});
690+
691+
return updatedSponsorTier;
692+
}
596693
}

src/backend/tests/unit/finance.test.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ describe('Finance Tests', () => {
320320
await createTestUser(wonderwomanGuest, orgId),
321321
'Silver',
322322
organization,
323-
'C0C0C0'
323+
'#C0C0C0',
324+
0
324325
)
325326
).rejects.toThrow(new AccessDeniedException('Only heads can create a sponsor tier'));
326327
});
@@ -330,12 +331,12 @@ describe('Finance Tests', () => {
330331
await createTestUser(batmanAppAdmin, orgId),
331332
'Silver',
332333
organization,
333-
'C0C0C0'
334+
'#C0C0C0',
335+
0
334336
);
335337

336338
expect(result.name).toEqual('Silver');
337-
expect(result.colorHexCode).toEqual('C0C0C0');
338-
expect(result.organizationId).toEqual(orgId);
339+
expect(result.colorHexCode).toEqual('#C0C0C0');
339340
});
340341
});
341342

@@ -474,14 +475,20 @@ describe('Finance Tests', () => {
474475
const result1 = await FinanceServices.getAllSponsorTiers(organization);
475476
expect(result1.length).toEqual(1);
476477

477-
await FinanceServices.createSponsorTier(await createTestUser(batmanAppAdmin, orgId), 'Silver', organization, 'C0C0C0');
478+
await FinanceServices.createSponsorTier(
479+
await createTestUser(batmanAppAdmin, orgId),
480+
'Silver',
481+
organization,
482+
'#C0C0C0',
483+
0
484+
);
478485

479486
const result2 = await FinanceServices.getAllSponsorTiers(organization);
480487
expect(result2.length).toEqual(2);
481488
expect(result2[0].name).toEqual('Gold Tier');
482489
expect(result2[0].colorHexCode).toEqual('#FFFFFF');
483490
expect(result2[1].name).toEqual('Silver');
484-
expect(result2[1].colorHexCode).toEqual('C0C0C0');
491+
expect(result2[1].colorHexCode).toEqual('#C0C0C0');
485492
});
486493
});
487494

src/frontend/src/apis/finance.api.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,3 +677,25 @@ export const editSponsor = (id: string, formData: SponsorPayload) => {
677677
export const deleteSponsorTask = (sponsorTaskId: string) => {
678678
return axios.post(apiUrls.deleteSponsorTask(sponsorTaskId));
679679
};
680+
681+
/**
682+
* API call to delete a given sponsor tier
683+
*
684+
* @param sponsorTierId the id of the sponsor tier to delete
685+
*
686+
* @returns the deleted sponsor tier
687+
*/
688+
export const deleteSponsorTier = (sponsorTierId: string) => {
689+
return axios.delete(apiUrls.deleteSponsorTier(sponsorTierId));
690+
};
691+
692+
/**
693+
* Edits a sponsor tier in the database
694+
* @param sponsorTierId the id of the sponsor tier
695+
* @param formData the data expected from the form
696+
* @return the updated sponsor tier
697+
*/
698+
699+
export const editSponsorTier = (sponsorTierId: string, formData: SponsorTierPayload) => {
700+
return axios.post(apiUrls.editSponsorTier(sponsorTierId), formData);
701+
};

src/frontend/src/components/WarningBanner.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ interface WarningBannerProps {
66
}
77

88
const WarningBanner: React.FC<WarningBannerProps> = ({ amount }) => {
9-
const formattedMessage = `Spending is $${amount} over budget!`;
9+
const formattedMessage = `Spending is $${Math.round(amount * 100) / 100} over budget!`;
1010

1111
return (
1212
<Box

0 commit comments

Comments
 (0)