Skip to content

Commit 06cbeda

Browse files
authored
Merge branch 'develop' into tonys-branch
2 parents 30f9d4d + 85f9531 commit 06cbeda

43 files changed

Lines changed: 766 additions & 749 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -437,13 +437,14 @@ export default class ProjectsController {
437437
static async editLinkType(req: Request, res: Response, next: NextFunction) {
438438
try {
439439
const { linkTypeName } = req.params;
440-
const { iconName, required } = req.body;
440+
const { name: newName, iconName, required } = req.body;
441441
const linkTypeUpdated = await ProjectsService.editLinkType(
442442
linkTypeName,
443443
iconName,
444444
required,
445445
req.currentUser,
446-
req.organization
446+
req.organization,
447+
newName
447448
);
448449
res.status(200).json(linkTypeUpdated);
449450
} catch (error: unknown) {

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,25 @@ export default class ReimbursementRequestsController {
497497
}
498498
}
499499

500+
static async setVendorTaxExemptStatus(req: Request, res: Response, next: NextFunction) {
501+
try {
502+
const { vendorId } = req.params;
503+
504+
const { taxExempt } = req.body;
505+
506+
const updatedVendor = await ReimbursementRequestService.setVendorTaxExemptStatus(
507+
vendorId,
508+
taxExempt,
509+
req.currentUser,
510+
req.organization
511+
);
512+
513+
res.status(200).json(updatedVendor);
514+
} catch (error) {
515+
next(error);
516+
}
517+
}
518+
500519
static async deleteVendor(req: Request, res: Response, next: NextFunction) {
501520
try {
502521
const { vendorId } = req.params;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ projectRouter.post(
2929
);
3030
projectRouter.post(
3131
'/link-types/:linkTypeName/edit',
32+
nonEmptyString(body('name').optional()),
3233
nonEmptyString(body('iconName')),
3334
body('required').isBoolean(),
3435
validateInputs,

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ reimbursementRequestsRouter.post(
101101
ReimbursementRequestController.editVendor
102102
);
103103

104+
reimbursementRequestsRouter.post(
105+
'/vendors/:vendorId/setTaxExemptStatus',
106+
ReimbursementRequestController.setVendorTaxExemptStatus
107+
);
108+
104109
reimbursementRequestsRouter.post('/:vendorId/vendors/delete', ReimbursementRequestController.deleteVendor);
105110

106111
reimbursementRequestsRouter.post(
@@ -167,7 +172,7 @@ reimbursementRequestsRouter.post(
167172
body('taxExempt').optional().isBoolean(),
168173
body('twoFactorContacts').optional().isArray(),
169174
nonEmptyString(body('twoFactorContacts.*')),
170-
nonEmptyString(body('notes')).optional(),
175+
body('notes').optional().isString(),
171176
validateInputs,
172177
ReimbursementRequestController.createVendor
173178
);

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ export default class ChangeRequestsService {
178178
dateReviewed: null
179179
},
180180
{
181-
NOT: { scopeChangeRequest: null }
181+
changes: { none: {} }
182182
}
183183
];
184184

@@ -666,9 +666,7 @@ export default class ChangeRequestsService {
666666
include: {
667667
changeRequests: {
668668
where: {
669-
dateDeleted: {
670-
not: null
671-
}
669+
dateDeleted: null
672670
},
673671
include: {
674672
changes: true

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,15 +1096,20 @@ export default class FinanceServices {
10961096

10971097
if (!tier) throw new NotFoundException('Sponsor Tier', sponsorTierId);
10981098

1099-
const existingSponsor = await prisma.sponsor.findFirst({
1100-
where: {
1101-
name,
1102-
organizationId: organization.organizationId
1103-
}
1104-
});
1099+
if (name !== oldSponsor.name) {
1100+
const existingSponsor = await prisma.sponsor.findFirst({
1101+
where: {
1102+
name: {
1103+
equals: name,
1104+
mode: 'insensitive'
1105+
},
1106+
organizationId: organization.organizationId
1107+
}
1108+
});
11051109

1106-
if (existingSponsor) {
1107-
throw new HttpException(400, `A sponsor with the name "${name}" already exists.`);
1110+
if (existingSponsor) {
1111+
throw new HttpException(400, `A sponsor with the name "${name}" already exists.`);
1112+
}
11081113
}
11091114

11101115
const updatedSponsor = await prisma.sponsor.update({

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,8 @@ export default class ProjectsService {
621621
iconName: string,
622622
required: boolean,
623623
submitter: User,
624-
organization: Organization
624+
organization: Organization,
625+
newName?: string
625626
): Promise<LinkType> {
626627
if (!(await userHasPermission(submitter.userId, organization.organizationId, isAdmin)))
627628
throw new AccessDeniedException('Only an admin can update the linkType');
@@ -638,11 +639,25 @@ export default class ProjectsService {
638639

639640
if (!linkType) throw new NotFoundException('Link Type', linkName);
640641

642+
// If attempting to rename, ensure new name does not conflict with an existing LinkType
643+
if (newName && newName !== linkName) {
644+
const existingWithNewName = await prisma.link_Type.findUnique({
645+
where: {
646+
uniqueLinkType: {
647+
name: newName,
648+
organizationId: organization.organizationId
649+
}
650+
}
651+
});
652+
653+
if (existingWithNewName) throw new HttpException(400, 'LinkType with that name already exists in this organization.');
654+
}
655+
641656
// update the LinkType
642657
const linkTypeUpdated = await prisma.link_Type.update({
643658
where: { id: linkType.id },
644659
data: {
645-
name: linkName,
660+
name: newName && newName ? newName : linkName,
646661
iconName,
647662
required
648663
}

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,6 +1544,42 @@ export default class ReimbursementRequestService {
15441544
return vendorTransformer(vendor);
15451545
}
15461546

1547+
static async setVendorTaxExemptStatus(
1548+
vendorId: string,
1549+
taxExempt: boolean,
1550+
submitter: User,
1551+
organization: Organization
1552+
): Promise<Vendor> {
1553+
const existingVendor = await prisma.vendor.findUnique({
1554+
where: { vendorId, dateDeleted: null },
1555+
include: { twoFactorContacts: { select: { userId: true } } }
1556+
});
1557+
1558+
if (!existingVendor) {
1559+
throw new NotFoundException('Vendor', vendorId);
1560+
}
1561+
1562+
if (existingVendor.organizationId !== organization.organizationId) {
1563+
throw new InvalidOrganizationException('Vendor');
1564+
}
1565+
1566+
const isUserAuthorized =
1567+
existingVendor.addedByUserId === submitter.userId ||
1568+
(await isUserOnFinanceTeam(submitter, organization.organizationId)) ||
1569+
(await userHasPermission(submitter.userId, organization.organizationId, isHead));
1570+
if (!isUserAuthorized) {
1571+
throw new AccessDeniedException(`You are not a member of the finance team!`);
1572+
}
1573+
1574+
const updatedVendor = await prisma.vendor.update({
1575+
where: { vendorId },
1576+
data: { taxExempt },
1577+
...getVendorQueryArgs(organization.organizationId)
1578+
});
1579+
1580+
return vendorTransformer(updatedVendor);
1581+
}
1582+
15471583
/**
15481584
* Deletes the vendor
15491585
*

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

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { alfred, robinMember, cyborgMember, theVisitorGuest } from '../test-data/users.test-data';
22
import ReimbursementRequestService from '../../src/services/reimbursement-requests.services';
3-
import { AccessDeniedException, DeletedException, HttpException, NotFoundException } from '../../src/utils/errors.utils';
3+
import {
4+
AccessDeniedException,
5+
DeletedException,
6+
HttpException,
7+
InvalidOrganizationException,
8+
NotFoundException
9+
} from '../../src/utils/errors.utils';
410
import { createTestReimbursementRequest, createTestUser, resetUsers } from '../test-utils';
511
import prisma from '../../src/prisma/prisma';
612
import { addDaysToDate, IndexCode, ReimbursementRequest, ReimbursementStatusType, AccountCode } from 'shared';
@@ -1041,4 +1047,82 @@ describe('Reimbursement Requests', () => {
10411047
expect(assignedRRs).toEqual([]);
10421048
});
10431049
});
1050+
1051+
describe('Set vendor tax exempt status', () => {
1052+
test('Finance member can set vendor tax exempt status', async () => {
1053+
const updatedVendor = await ReimbursementRequestService.setVendorTaxExemptStatus(
1054+
createdVendor.vendorId,
1055+
true,
1056+
financeMember,
1057+
org
1058+
);
1059+
1060+
expect(updatedVendor).not.toBeNull();
1061+
expect(updatedVendor.taxExempt).toBe(true);
1062+
});
1063+
1064+
test('Non-finance member cannot set vendor tax exempt status', async () => {
1065+
await expect(
1066+
ReimbursementRequestService.setVendorTaxExemptStatus(createdVendor.vendorId, true, regularMember, org)
1067+
).rejects.toThrow(new AccessDeniedException('You are not a member of the finance team!'));
1068+
});
1069+
1070+
test('Cannot set tax exempt status for non-existent vendor', async () => {
1071+
await expect(
1072+
ReimbursementRequestService.setVendorTaxExemptStatus('non-existent-id', true, financeMember, org)
1073+
).rejects.toThrow(new NotFoundException('Vendor', 'non-existent-id'));
1074+
});
1075+
1076+
test('Cannot set tax exempt status for vendor in different organization', async () => {
1077+
// Create a vendor in a different organization
1078+
const otherOrg = await prisma.organization.create({
1079+
data: {
1080+
name: 'Other Org',
1081+
userCreated: { connect: { userId: financeHead.userId } }
1082+
}
1083+
});
1084+
1085+
const otherMember: User = await prisma.user.create({
1086+
data: {
1087+
firstName: 'Other',
1088+
lastName: 'Member',
1089+
googleAuthId: '99',
1090+
email: 'email@email.other',
1091+
roles: {
1092+
create: {
1093+
roleType: Role_Type.MEMBER,
1094+
organization: {
1095+
connect: { organizationId: otherOrg.organizationId }
1096+
}
1097+
}
1098+
}
1099+
}
1100+
});
1101+
1102+
const otherVendor = await ReimbursementRequestService.createVendor(
1103+
otherMember,
1104+
'Other Org Vendor',
1105+
otherOrg,
1106+
false,
1107+
[],
1108+
'Some notes'
1109+
);
1110+
1111+
await expect(
1112+
ReimbursementRequestService.setVendorTaxExemptStatus(otherVendor.vendorId, true, financeMember, org)
1113+
).rejects.toThrow(new InvalidOrganizationException('Vendor'));
1114+
});
1115+
1116+
test('head can set vendor tax exempt status', async () => {
1117+
const updatedVendor = await ReimbursementRequestService.setVendorTaxExemptStatus(
1118+
createdVendor.vendorId,
1119+
true,
1120+
financeHead,
1121+
org
1122+
);
1123+
1124+
expect(updatedVendor).not.toBeNull();
1125+
expect(updatedVendor.taxExempt).toBe(true);
1126+
});
1127+
});
10441128
});

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,16 @@ export const editVendor = async (id: string, vendorData: EditVendorPayload) => {
441441
return axios.post(apiUrls.financeEditVendor(id), vendorData);
442442
};
443443

444+
/**
445+
* API call to set the tax exempt status of a vendor
446+
* @param vendorId id of vendor to set
447+
* @param taxExempt whether the vendor is tax exempt
448+
* @returns updated vendor
449+
*/
450+
export const setTaxExemptStatus = async (vendorId: string, taxExempt: boolean) => {
451+
return axios.post(apiUrls.financeSetVendorTaxExemptStatus(vendorId), { taxExempt });
452+
};
453+
444454
/**
445455
* API call to delete a given vendor
446456
*

0 commit comments

Comments
 (0)