Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
62ca4fb
get approved and update volunteer endpoints
swarkewalia Dec 3, 2025
8907ee8
merge main
swarkewalia Dec 3, 2025
08d4047
merge
swarkewalia Dec 13, 2025
d2aef46
merge fixes
swarkewalia Dec 13, 2025
63fa43a
fix typeorm
swarkewalia Dec 13, 2025
265bad6
merge fix
swarkewalia Jan 18, 2026
1c948a7
Merge branch 'sk/SSF-97-pantry-management-backend' of https://github.…
swarkewalia Jan 20, 2026
ab18d04
Merge branch 'main' of https://github.com/Code-4-Community/ssf into s…
swarkewalia Jan 21, 2026
4256a27
Merge branch 'sk/SSF-97-pantry-management-backend' of https://github.…
swarkewalia Jan 21, 2026
fb5ac86
merge fix
swarkewalia Jan 21, 2026
3489633
gitignore fix
swarkewalia Jan 22, 2026
72fc25d
git ignore fix 2
swarkewalia Jan 22, 2026
ddda63f
auth service fix
swarkewalia Jan 22, 2026
ba519d2
extra files fixes
swarkewalia Jan 22, 2026
1681f42
originally changed files
swarkewalia Jan 22, 2026
fe9f0fd
updated package.json
swarkewalia Jan 24, 2026
b1fffbb
prettier
swarkewalia Jan 25, 2026
0f9df9c
fixed yarn.lock
swarkewalia Jan 25, 2026
af4fea6
add more approved pantry info
swarkewalia Jan 27, 2026
66a7677
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Jan 27, 2026
85a317f
get endpoint test
swarkewalia Jan 27, 2026
0094616
Merge branch 'sk/SSF-97-pantry-management-backend' of https://github.…
swarkewalia Jan 27, 2026
842ccf0
updatepantryvolunteers endpoint
swarkewalia Jan 27, 2026
8f25130
updates
swarkewalia Jan 28, 2026
ae4f126
more changes
swarkewalia Jan 30, 2026
31cb4e5
nit fixes
swarkewalia Feb 3, 2026
20bd058
fixes
swarkewalia Feb 3, 2026
a8a3289
Merge branch 'main' of https://github.com/Code-4-Community/ssf into s…
swarkewalia Feb 3, 2026
dc53864
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Feb 4, 2026
7fe16c0
add volunteers assigned to pantry
swarkewalia Feb 4, 2026
6aa3025
Merge branch 'main' of https://github.com/Code-4-Community/ssf into s…
swarkewalia Feb 5, 2026
18776f3
merge
swarkewalia Feb 12, 2026
b0591a0
fixed more changes
swarkewalia Feb 13, 2026
f4e3893
dependency issues
swarkewalia Feb 13, 2026
d29748f
final fixes
swarkewalia Feb 15, 2026
7a66455
merge
swarkewalia Feb 15, 2026
7f6edf9
nit
swarkewalia Feb 15, 2026
cf9f2b6
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Feb 17, 2026
82ea854
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Feb 17, 2026
e88b4bd
fix git tests
swarkewalia Feb 20, 2026
303c5c2
conflicts
swarkewalia Feb 20, 2026
b9e4491
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Feb 20, 2026
934e9db
fix conflict
swarkewalia Feb 20, 2026
9f5e2e5
merge
swarkewalia Feb 22, 2026
ff491af
add shipmentstate
swarkewalia Feb 22, 2026
97fbb5d
prettier
swarkewalia Feb 22, 2026
994bf06
fix merge
swarkewalia Feb 22, 2026
0a78425
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Feb 22, 2026
1d25739
yurika comments
swarkewalia Mar 1, 2026
92f19ab
merge
swarkewalia Mar 1, 2026
88a0f2a
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Mar 1, 2026
15dce5d
merge changes
swarkewalia Mar 1, 2026
a6cae91
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Mar 6, 2026
5ad50d1
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Mar 8, 2026
c0f9961
removed extra files from pr
swarkewalia Mar 14, 2026
0e4a0ce
merge
swarkewalia Mar 14, 2026
51b9b69
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Mar 14, 2026
6831f31
Merge branch 'main' of https://github.com/Code-4-Community/ssf into s…
swarkewalia Mar 14, 2026
33f0a11
Merge branch 'sk/SSF-97-pantry-management-backend' of https://github.…
swarkewalia Mar 14, 2026
2a5b86f
Merge branch 'main' of https://github.com/Code-4-Community/ssf into s…
swarkewalia Mar 18, 2026
d9135a3
fix tests
swarkewalia Mar 18, 2026
4449e5b
Merge branch 'main' into sk/SSF-97-pantry-management-backend
swarkewalia Mar 20, 2026
219f7b5
fixes
swarkewalia Mar 20, 2026
b1faff8
Merge branch 'sk/SSF-97-pantry-management-backend' of https://github.…
swarkewalia Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions apps/backend/src/pantries/pantries.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
RefrigeratedDonation,
ReserveFoodForAllergic,
ServeAllergicChildren,
ApprovedPantryResponse,
TotalStats,
} from './types';
import { EmailsService } from '../emails/email.service';
Expand Down Expand Up @@ -255,6 +256,60 @@ describe('PantriesController', () => {
});
});

describe('getApprovedPantries', () => {
it('should return approved pantries with volunteers', async () => {
const mockApprovedPantries: ApprovedPantryResponse[] = [
{
pantryId: 1,
pantryName: 'Community Food Pantry',
refrigeratedDonation: RefrigeratedDonation.YES,
volunteers: [
{
userId: 10,
firstName: 'Alice',
lastName: 'Johnson',
email: 'alice.johnson@example.com',
phone: '(617) 555-0100',
},
{
userId: 11,
firstName: 'Bob',
lastName: 'Williams',
email: 'bob.williams@example.com',
phone: '(617) 555-0101',
},
],
},
];

mockPantriesService.getApprovedPantriesWithVolunteers.mockResolvedValue(
mockApprovedPantries,
);

const result = await controller.getApprovedPantries();

expect(result).toEqual(mockApprovedPantries);
expect(
mockPantriesService.getApprovedPantriesWithVolunteers,
).toHaveBeenCalledTimes(1);
});
});

describe('updatePantryVolunteers', () => {
it('should overwrite the set of volunteers assigned to a pantry', async () => {
const pantryId = 1;
const volunteerIds = [10, 11, 12];

mockPantriesService.updatePantryVolunteers.mockResolvedValue(undefined);

await controller.updatePantryVolunteers(pantryId, volunteerIds);

expect(mockPantriesService.updatePantryVolunteers).toHaveBeenCalledWith(
pantryId,
volunteerIds,
);
});
});
describe('getCurrentUserPantryId', () => {
it('returns pantryId for authenticated user', async () => {
const req = { user: { id: 1 } };
Expand Down
19 changes: 18 additions & 1 deletion apps/backend/src/pantries/pantries.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import {
Param,
ParseIntPipe,
Patch,
Put,
Post,
Query,
Req,
ValidationPipe,
} from '@nestjs/common';
import { Pantry } from './pantries.entity';
import { PantriesService } from './pantries.service';
import { Role } from '../users/types';
import { Roles } from '../auth/roles.decorator';
import { ValidationPipe } from '@nestjs/common';
import { PantryApplicationDto } from './dtos/pantry-application.dto';
import { ApiBody } from '@nestjs/swagger';
import {
Expand All @@ -24,6 +25,7 @@ import {
RefrigeratedDonation,
ReserveFoodForAllergic,
ServeAllergicChildren,
ApprovedPantryResponse,
TotalStats,
} from './types';
import { Order } from '../orders/order.entity';
Expand Down Expand Up @@ -75,6 +77,12 @@ export class PantriesController {
return this.pantriesService.getPendingPantries();
}

@Roles(Role.ADMIN)
@Get('/approved')
async getApprovedPantries(): Promise<ApprovedPantryResponse[]> {
return this.pantriesService.getApprovedPantriesWithVolunteers();
}

@CheckOwnership({
idParam: 'pantryId',
resolver: async ({ entityId, services }) => {
Expand Down Expand Up @@ -371,4 +379,13 @@ export class PantriesController {
attachments,
);
}

@Roles(Role.ADMIN)
@Put('/:pantryId/volunteers')
async updatePantryVolunteers(
@Param('pantryId', ParseIntPipe) pantryId: number,
@Body('volunteerIds') volunteerIds: number[],
): Promise<void> {
return this.pantriesService.updatePantryVolunteers(pantryId, volunteerIds);
}
}
2 changes: 1 addition & 1 deletion apps/backend/src/pantries/pantries.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export class Pantry {
name: 'status',
type: 'enum',
enum: ApplicationStatus,
enumName: 'application_status_enum',
enumName: 'pantries_status_enum',
})
status!: ApplicationStatus;

Expand Down
111 changes: 111 additions & 0 deletions apps/backend/src/pantries/pantries.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ describe('PantriesService', () => {
shipmentAddressCity: 'Testville',
shipmentAddressState: 'TX',
shipmentAddressZip: '11111',
shipmentAddressCountry: 'USA',
mailingAddressLine1: '1 Test St',
mailingAddressCity: 'Testville',
mailingAddressState: 'TX',
Expand Down Expand Up @@ -575,4 +576,114 @@ describe('PantriesService', () => {
);
});
});

describe('getApprovedPantriesWithVolunteers', () => {
it('should return approved pantries with mapped volunteer info', async () => {
const result = await service.getApprovedPantriesWithVolunteers();

expect(result.length).toBeGreaterThan(0);
expect(result.every((p) => p.pantryId)).toBe(true);
expect(result.every((p) => p.pantryName)).toBe(true);
result.forEach((p) => {
expect(p.volunteers).toBeDefined();
p.volunteers.forEach((v) => {
expect(v.userId).toBeDefined();
expect(v.firstName).toBeDefined();
expect(v.lastName).toBeDefined();
expect(v.email).toBeDefined();
expect(v.phone).toBeDefined();
});
});
});

it('should return empty volunteers array when pantry has no volunteers', async () => {
await service.addPantry({
contactFirstName: 'Test',
contactLastName: 'Pantry',
contactEmail: 'test.novolunteers@example.com',
contactPhone: '555-000-9999',
hasEmailContact: false,
pantryName: 'No Volunteer Pantry',
shipmentAddressLine1: '1 Test St',
shipmentAddressCity: 'Boston',
shipmentAddressState: 'MA',
shipmentAddressZip: '02101',
mailingAddressLine1: '1 Test St',
mailingAddressCity: 'Boston',
mailingAddressState: 'MA',
mailingAddressZip: '02101',
allergenClients: 'none',
restrictions: ['none'],
refrigeratedDonation: RefrigeratedDonation.NO,
acceptFoodDeliveries: false,
reserveFoodForAllergic: ReserveFoodForAllergic.NO,
dedicatedAllergyFriendly: false,
activities: [Activity.CREATE_LABELED_SHELF],
itemsInStock: 'none',
needMoreOptions: 'none',
} as PantryApplicationDto);

const saved = await testDataSource.getRepository(Pantry).findOne({
where: { pantryName: 'No Volunteer Pantry' },
});
await testDataSource.getRepository(Pantry).update(saved!.pantryId, {
status: ApplicationStatus.APPROVED,
});

const result = await service.getApprovedPantriesWithVolunteers();
const pantryWithNoVolunteers = result.find(
(p) => p.pantryName === 'No Volunteer Pantry',
);
expect(pantryWithNoVolunteers).toBeDefined();
expect(pantryWithNoVolunteers?.volunteers).toEqual([]);
});

it('should return empty array when no approved pantries exist', async () => {
await testDataSource.query(
`UPDATE pantries SET status = 'pending' WHERE status = 'approved'`,
);
const result = await service.getApprovedPantriesWithVolunteers();
expect(result).toEqual([]);
});
});

describe('updatePantryVolunteers', () => {
const getVolunteerId = async (email: string) =>
(
await testDataSource.query(
`SELECT user_id FROM users WHERE email = $1 LIMIT 1`,
[email],
)
)[0].user_id;

it('replaces volunteer set', async () => {
const williamId = Number(await getVolunteerId('william.m@volunteer.org'));
await service.updatePantryVolunteers(1, [williamId]);
const pantry = await testDataSource
.getRepository(Pantry)
.findOne({ where: { pantryId: 1 }, relations: ['volunteers'] });
expect(pantry?.volunteers).toHaveLength(1);
expect(pantry?.volunteers?.[0].id).toBe(williamId);
});

it('throws NotFoundException when pantry not found', async () => {
const williamId = Number(await getVolunteerId('william.m@volunteer.org'));
await expect(
service.updatePantryVolunteers(9999, [williamId]),
).rejects.toThrow(NotFoundException);
});

it('throws NotFoundException when volunteer id does not exist', async () => {
await expect(service.updatePantryVolunteers(1, [99999])).rejects.toThrow(
NotFoundException,
);
});

it('throws BadRequestException when user is not a volunteer', async () => {
const adminId = Number(await getVolunteerId('john.smith@ssf.org'));
await expect(
service.updatePantryVolunteers(1, [adminId]),
).rejects.toThrow(BadRequestException);
});
});
});
59 changes: 59 additions & 0 deletions apps/backend/src/pantries/pantries.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { validateId } from '../utils/validation.utils';
import { ApplicationStatus } from '../shared/types';
import { PantryApplicationDto } from './dtos/pantry-application.dto';
import { Role } from '../users/types';
import { ApprovedPantryResponse } from './types';
import { PantryStats, TotalStats } from './types';
import { userSchemaDto } from '../users/dtos/userSchema.dto';
import { UsersService } from '../users/users.service';
Expand Down Expand Up @@ -362,6 +363,64 @@ export class PantriesService {
await this.repo.update(id, { status: ApplicationStatus.DENIED });
}

async getApprovedPantriesWithVolunteers(): Promise<ApprovedPantryResponse[]> {
const pantries = await this.repo.find({
where: { status: ApplicationStatus.APPROVED },
relations: ['volunteers', 'pantryUser'],
});

return pantries.map((pantry) => ({
Copy link
Collaborator

@sam-schu sam-schu Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you take a look at what the return from the above call to TypeORM looks like? That should likely have all the information we need, it doesn't look like this map is doing much other than copying all the fields from one object to another. If we do go with the current approach rather than just returning pantries (with volunteers' cognito subs removed), we should make things more concise by limiting to the fields that are actually needed by the admin pantry management page this endpoint is for

pantryId: pantry.pantryId,
pantryName: pantry.pantryName,
refrigeratedDonation: pantry.refrigeratedDonation,
volunteers: (pantry.volunteers || []).map((volunteer) => ({
userId: volunteer.id,
firstName: volunteer.firstName,
lastName: volunteer.lastName,
email: volunteer.email,
phone: volunteer.phone,
})),
}));
}

async updatePantryVolunteers(
pantryId: number,
volunteerIds: number[],
): Promise<void> {
validateId(pantryId, 'Pantry');
volunteerIds.forEach((id) => validateId(id, 'Volunteer'));

const pantry = await this.repo.findOne({
where: { pantryId },
relations: ['volunteers'],
});

if (!pantry) {
throw new NotFoundException(`Pantry with ID ${pantryId} not found`);
}

const users = await Promise.all(
volunteerIds.map((id) => this.usersService.findOne(id)),
);

if (users.length !== volunteerIds.length) {
throw new NotFoundException('One or more users not found');
}

const nonVolunteers = users.filter((user) => user.role !== Role.VOLUNTEER);

if (nonVolunteers.length > 0) {
throw new BadRequestException(
`Users ${nonVolunteers
.map((user) => user.id)
.join(', ')} are not volunteers`,
);
}

pantry.volunteers = users;
await this.repo.save(pantry);
}

async findByIds(pantryIds: number[]): Promise<Pantry[]> {
pantryIds.forEach((id) => validateId(id, 'Pantry'));

Expand Down
15 changes: 15 additions & 0 deletions apps/backend/src/pantries/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
export interface ApprovedPantryResponse {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are going to need more info than this for our approved pantry response. For one, all these pantries are approved so we shouldnt need the status. Additionally, check out the Figma for the frontend: https://www.figma.com/design/brc5luMhizIFp893XIutYe/SP26---SSF-Designs?node-id=756-11085&t=3UiKr0MdYxCcdUUK-0

The modal that pops up with Pantry Details should tell you what information we need.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, there may have been some confusion here. It's fine if we have to make another API call to get the full details about a pantry when the user navigates to the "pantry details" page - we just need the info for the main table (as well as the IDs necessary to be able to navigate to the other pages)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, there may have been some confusion here. It's fine if we have to make another API call to get the full details about a pantry when the user navigates to the "pantry details" page - we just need the info for the main table (as well as the IDs necessary to be able to navigate to the other pages)

bringing this back - let's just return the pantry id, name, refrigerated donation, and volunteers assigned list for this backend supporting the pantry management view

for the pantry details page (not part of this ticket), we can call the existing get pantry by pantryId endpoint for all these fields

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these interfaces look very similar to the Pantry and User entity types. Can we define the return types using those rather than re-listing out all their fields?

pantryId: number;
pantryName: string;
refrigeratedDonation: RefrigeratedDonation;
volunteers: AssignedVolunteer[];
}

export interface AssignedVolunteer {
userId: number;
firstName: string;
lastName: string;
email: string;
phone: string;
}

export enum RefrigeratedDonation {
YES = 'Yes, always',
NO = 'No',
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface Pantry {
shipmentAddressCity: string;
shipmentAddressState: string;
shipmentAddressZip: string;
shipmentAddressCountry?: string;
shipmentAddressCountry: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry - this should be nullable (restore previous ver)

mailingAddressLine1: string;
mailingAddressLine2?: string;
mailingAddressCity: string;
Expand Down
Loading