From 50b0bc362806a111d5eb5005cdcf4cd70df9b4a9 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:58:25 -0400 Subject: [PATCH 01/18] volunteer backend updates minus tests --- apps/backend/src/auth/auth.module.ts | 11 +--- apps/backend/src/config/typeorm.ts | 2 + ...4668-AddVolunteerPantryUniqueConstraint.ts | 48 ++++++++++++++ apps/backend/src/users/users.controller.ts | 17 ++++- apps/backend/src/users/users.module.ts | 6 +- apps/backend/src/users/users.service.ts | 63 ++++++++++++++++++- .../volunteerAssignments.entity.ts | 20 +++--- .../volunteerAssignments.service.ts | 3 +- apps/frontend/src/types/types.ts | 1 - 9 files changed, 141 insertions(+), 30 deletions(-) create mode 100644 apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index 09f5965c..eac8a5b6 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -1,19 +1,14 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { PassportModule } from '@nestjs/passport'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; -import { UsersService } from '../users/users.service'; -import { User } from '../users/user.entity'; import { JwtStrategy } from './jwt.strategy'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [ - TypeOrmModule.forFeature([User]), - PassportModule.register({ defaultStrategy: 'jwt' }), - ], + imports: [UsersModule, PassportModule.register({ defaultStrategy: 'jwt' })], controllers: [AuthController], - providers: [AuthService, UsersService, JwtStrategy], + providers: [AuthService, JwtStrategy], }) export class AuthModule {} diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index b356b698..dc992819 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -20,6 +20,7 @@ import { AddingEnumValues1760538239997 } from '../migrations/1760538239997-Addin import { UpdateColsToUseEnumType1760886499863 } from '../migrations/1760886499863-UpdateColsToUseEnumType'; import { UpdatePantriesTable1742739750279 } from '../migrations/1742739750279-updatePantriesTable'; import { RemoveOrdersDonationId1761500262238 } from '../migrations/1761500262238-RemoveOrdersDonationId'; +import { AddVolunteerPantryUniqueConstraint1760033134668 } from '../migrations/1760033134668-AddVolunteerPantryUniqueConstraint'; import { AllergyFriendlyToBoolType1763963056712 } from '../migrations/1763963056712-AllergyFriendlyToBoolType'; import { UpdatePantryUserFieldsFixed1764350314832 } from '../migrations/1764350314832-UpdatePantryUserFieldsFixed'; @@ -55,6 +56,7 @@ const config = { UpdateColsToUseEnumType1760886499863, UpdatePantriesTable1742739750279, RemoveOrdersDonationId1761500262238, + AddVolunteerPantryUniqueConstraint1760033134668, AllergyFriendlyToBoolType1763963056712, UpdatePantryUserFieldsFixed1764350314832, ], diff --git a/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts new file mode 100644 index 00000000..de9dcd59 --- /dev/null +++ b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts @@ -0,0 +1,48 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddVolunteerPantryUniqueConstraint1760033134668 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE volunteer_assignments DROP COLUMN assignment_id; + `); + + await queryRunner.query(` + ALTER TABLE volunteer_assignments + ADD PRIMARY KEY (volunteer_id, pantry_id); + `); + + await queryRunner.query(` + ALTER TABLE volunteer_assignments DROP CONSTRAINT IF EXISTS fk_volunteer_id; + `); + + await queryRunner.query(` + ALTER TABLE volunteer_assignments DROP CONSTRAINT IF EXISTS fk_pantry_id; + `); + + await queryRunner.query(` + ALTER TABLE volunteer_assignments + ADD CONSTRAINT fk_volunteer_id FOREIGN KEY (volunteer_id) REFERENCES users(user_id) ON DELETE CASCADE, + ADD CONSTRAINT fk_pantry_id FOREIGN KEY (pantry_id) REFERENCES pantries(pantry_id) ON DELETE CASCADE; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE volunteer_assignments DROP CONSTRAINT fk_volunteer_id; + `); + + await queryRunner.query(` + ALTER TABLE volunteer_assignments DROP CONSTRAINT fk_pantry_id; + `); + + await queryRunner.query(` + ALTER TABLE volunteer_assignments DROP CONSTRAINT volunteer_assignments_pkey; + `); + + await queryRunner.query(` + ALTER TABLE volunteer_assignments ADD COLUMN assignment_id SERIAL PRIMARY KEY; + `); + } +} diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index 93a4dc01..b0e7f6e5 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -6,6 +6,7 @@ import { ParseIntPipe, Put, Post, + Patch, BadRequestException, Body, //UseGuards, @@ -15,7 +16,6 @@ import { UsersService } from './users.service'; //import { AuthGuard } from '@nestjs/passport'; import { User } from './user.entity'; import { Role } from './types'; -import { VOLUNTEER_ROLES } from './types'; import { userSchemaDto } from './dtos/userSchema.dto'; //import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; @@ -26,7 +26,7 @@ export class UsersController { @Get('/volunteers') async getAllVolunteers(): Promise { - return this.usersService.findUsersByRoles(VOLUNTEER_ROLES); + return this.usersService.getVolunteersAndPantryAssignments(); } // @UseGuards(AuthGuard('jwt')) @@ -56,4 +56,17 @@ export class UsersController { const { email, firstName, lastName, phone, role } = createUserDto; return this.usersService.create(email, firstName, lastName, phone, role); } + + @Get('/:id/pantries') + async getVolunteerPantries(@Param('id', ParseIntPipe) id: number) { + return this.usersService.getVolunteerPantries(id); + } + + @Patch(':id/pantries') + async assignPantry( + @Param('id', ParseIntPipe) id: number, + @Body('pantryIds') pantryIds: number[], + ) { + return this.usersService.assignPantriesToVolunteer(id, pantryIds); + } } diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index 2f78bb05..a5c98467 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -6,10 +6,12 @@ import { User } from './user.entity'; import { JwtStrategy } from '../auth/jwt.strategy'; import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; import { AuthService } from '../auth/auth.service'; +import { Assignments } from '../volunteerAssignments/volunteerAssignments.entity'; +import { Pantry } from '../pantries/pantries.entity'; @Module({ - imports: [TypeOrmModule.forFeature([User])], - exports: [UsersService], + imports: [TypeOrmModule.forFeature([User, Assignments, Pantry])], + exports: [UsersService, TypeOrmModule], controllers: [UsersController], providers: [UsersService, AuthService, JwtStrategy, CurrentUserInterceptor], }) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index f10a0237..98529420 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -1,14 +1,26 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { User } from './user.entity'; -import { Role } from './types'; +import { Role, VOLUNTEER_ROLES } from './types'; import { validateId } from '../utils/validation.utils'; +import { Assignments } from '../volunteerAssignments/volunteerAssignments.entity'; +import { Pantry } from '../pantries/pantries.entity'; @Injectable() export class UsersService { - constructor(@InjectRepository(User) private repo: Repository) {} + constructor( + @InjectRepository(User) + private repo: Repository, + + @InjectRepository(Assignments) + private assignmentsRepo: Repository, + ) {} async create( email: string, @@ -72,4 +84,49 @@ export class UsersService { async findUsersByRoles(roles: Role[]): Promise { return this.repo.find({ where: { role: In(roles) } }); } + + async getVolunteersAndPantryAssignments() { + const volunteers = await this.findUsersByRoles(VOLUNTEER_ROLES); + + const assignments = await this.assignmentsRepo.find({ + relations: ['pantry', 'volunteer'], + }); + + return volunteers.map((v) => { + const assigned = assignments + .filter((a) => a.volunteer.id == v.id) + .map((a) => a.pantry.pantryId); + return { ...v, pantryIds: assigned }; + }); + } + + async getVolunteerPantries(volunteerId: number) { + validateId(volunteerId, 'Volunteer'); + const assignments = await this.assignmentsRepo.find({ + where: { volunteer: { id: volunteerId } }, + relations: ['pantry'], + }); + + return assignments.map((a) => a.pantry); + } + + async assignPantriesToVolunteer(volunteerId: number, pantryIds: number[]) { + validateId(volunteerId, 'Volunteer'); + const volunteer = await this.repo.findOne({ where: { id: volunteerId } }); + if (!volunteer) + throw new NotFoundException(`Volunteer ${volunteerId} not found`); + + const pantryRepo = this.assignmentsRepo.manager.getRepository(Pantry); + const pantries = await pantryRepo.findBy({ pantryId: In(pantryIds) }); + + if (pantries.length !== pantryIds.length) { + throw new BadRequestException('One or more pantries not found'); + } + + const assignments = pantries.map((pantry) => + this.assignmentsRepo.create({ volunteer, pantry }), + ); + + return this.assignmentsRepo.save(assignments); + } } diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts index d9929fe5..9d7bca73 100644 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts +++ b/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts @@ -1,27 +1,23 @@ -import { - Entity, - PrimaryGeneratedColumn, - OneToOne, - JoinColumn, - ManyToOne, - Column, -} from 'typeorm'; +import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; import { User } from '../users/user.entity'; import { Pantry } from '../pantries/pantries.entity'; @Entity('volunteer_assignments') export class Assignments { - @PrimaryGeneratedColumn({ name: 'assignment_id' }) - assignmentId: number; + @PrimaryColumn({ name: 'volunteer_id' }) + volunteerId: number; - @ManyToOne(() => User, { nullable: false }) + @PrimaryColumn({ name: 'pantry_id' }) + pantryId: number; + + @ManyToOne(() => User, { nullable: false, onDelete: 'CASCADE' }) @JoinColumn({ name: 'volunteer_id', referencedColumnName: 'id', }) volunteer: User; - @OneToOne(() => Pantry, { nullable: true }) + @ManyToOne(() => Pantry, { nullable: false, onDelete: 'CASCADE' }) @JoinColumn({ name: 'pantry_id', referencedColumnName: 'pantryId', diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts index d6ef19a1..d20fff30 100644 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts +++ b/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts @@ -11,12 +11,11 @@ export class AssignmentsService { private usersService: UsersService, ) {} - // Gets the assignment id, volunteer details and the corresponding pantry + // Gets the volunteer details and the corresponding pantry async getAssignments() { const results = await this.repo.find({ relations: ['volunteer', 'pantry'], select: { - assignmentId: true, volunteer: { id: true, firstName: true, diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index cb901272..6c179b8e 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -174,7 +174,6 @@ export enum VolunteerType { } export interface VolunteerPantryAssignment { - assignmentId: number; volunteer: { id: number; firstName: string; From e54b6a32cdb83c137d1c1a56d1ffa9c0016c7c75 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sat, 11 Oct 2025 15:34:42 -0400 Subject: [PATCH 02/18] review comments --- ...033134668-AddVolunteerPantryUniqueConstraint.ts | 14 -------------- apps/backend/src/users/users.service.ts | 10 ++++++++-- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts index de9dcd59..90e94be5 100644 --- a/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts +++ b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts @@ -6,22 +6,14 @@ export class AddVolunteerPantryUniqueConstraint1760033134668 public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE volunteer_assignments DROP COLUMN assignment_id; - `); - await queryRunner.query(` ALTER TABLE volunteer_assignments ADD PRIMARY KEY (volunteer_id, pantry_id); - `); - await queryRunner.query(` ALTER TABLE volunteer_assignments DROP CONSTRAINT IF EXISTS fk_volunteer_id; - `); - await queryRunner.query(` ALTER TABLE volunteer_assignments DROP CONSTRAINT IF EXISTS fk_pantry_id; - `); - await queryRunner.query(` ALTER TABLE volunteer_assignments ADD CONSTRAINT fk_volunteer_id FOREIGN KEY (volunteer_id) REFERENCES users(user_id) ON DELETE CASCADE, ADD CONSTRAINT fk_pantry_id FOREIGN KEY (pantry_id) REFERENCES pantries(pantry_id) ON DELETE CASCADE; @@ -31,17 +23,11 @@ export class AddVolunteerPantryUniqueConstraint1760033134668 public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE volunteer_assignments DROP CONSTRAINT fk_volunteer_id; - `); - await queryRunner.query(` ALTER TABLE volunteer_assignments DROP CONSTRAINT fk_pantry_id; - `); - await queryRunner.query(` ALTER TABLE volunteer_assignments DROP CONSTRAINT volunteer_assignments_pkey; - `); - await queryRunner.query(` ALTER TABLE volunteer_assignments ADD COLUMN assignment_id SERIAL PRIMARY KEY; `); } diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 98529420..bef91385 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -20,6 +20,9 @@ export class UsersService { @InjectRepository(Assignments) private assignmentsRepo: Repository, + + @InjectRepository(Pantry) + private pantryRepo: Repository, ) {} async create( @@ -112,12 +115,15 @@ export class UsersService { async assignPantriesToVolunteer(volunteerId: number, pantryIds: number[]) { validateId(volunteerId, 'Volunteer'); + for (const pantryId of pantryIds) { + validateId(pantryId, 'Pantry'); + } + const volunteer = await this.repo.findOne({ where: { id: volunteerId } }); if (!volunteer) throw new NotFoundException(`Volunteer ${volunteerId} not found`); - const pantryRepo = this.assignmentsRepo.manager.getRepository(Pantry); - const pantries = await pantryRepo.findBy({ pantryId: In(pantryIds) }); + const pantries = await this.pantryRepo.findBy({ pantryId: In(pantryIds) }); if (pantries.length !== pantryIds.length) { throw new BadRequestException('One or more pantries not found'); From d8921384ba64c20faf71671b81547ef62551be93 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:41:15 -0500 Subject: [PATCH 03/18] review comments --- .../src/pantries/pantries.controller.ts | 1 + apps/backend/src/users/users.controller.ts | 23 ++++++++-------- apps/backend/src/users/users.module.ts | 11 ++++---- apps/backend/src/users/users.service.spec.ts | 26 +++++++++++++++++++ apps/backend/src/users/users.service.ts | 22 +++++++++------- .../volunteerAssignments.controller.ts | 6 ++--- .../volunteerAssignments.entity.ts | 2 +- .../volunteerAssignments.module.ts | 5 ++-- .../volunteerAssignments.service.ts | 6 ++--- apps/frontend/src/types/types.ts | 16 +++--------- 10 files changed, 67 insertions(+), 51 deletions(-) diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index ee8287ce..0bcd6484 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -204,6 +204,7 @@ export class PantriesController { ], }, }) + @Post() async submitPantryApplication( @Body(new ValidationPipe()) diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index b0e7f6e5..c24751ce 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -6,7 +6,6 @@ import { ParseIntPipe, Put, Post, - Patch, BadRequestException, Body, //UseGuards, @@ -25,7 +24,7 @@ export class UsersController { constructor(private usersService: UsersService) {} @Get('/volunteers') - async getAllVolunteers(): Promise { + async getAllVolunteers() { return this.usersService.getVolunteersAndPantryAssignments(); } @@ -35,16 +34,21 @@ export class UsersController { return this.usersService.findOne(userId); } + @Get('/:id/pantries') + async getVolunteerPantries(@Param('id', ParseIntPipe) id: number) { + return this.usersService.getVolunteerPantries(id); + } + @Delete('/:id') - removeUser(@Param('id', ParseIntPipe) userId: number) { + removeUser(@Param('id', ParseIntPipe) userId: number): Promise { return this.usersService.remove(userId); } - @Put(':id/role') + @Put('/:id/role') async updateRole( @Param('id', ParseIntPipe) id: number, @Body('role') role: string, - ) { + ): Promise { if (!Object.values(Role).includes(role as Role)) { throw new BadRequestException('Invalid role'); } @@ -57,13 +61,8 @@ export class UsersController { return this.usersService.create(email, firstName, lastName, phone, role); } - @Get('/:id/pantries') - async getVolunteerPantries(@Param('id', ParseIntPipe) id: number) { - return this.usersService.getVolunteerPantries(id); - } - - @Patch(':id/pantries') - async assignPantry( + @Post('/:id/pantries') + async assignPantries( @Param('id', ParseIntPipe) id: number, @Body('pantryIds') pantryIds: number[], ) { diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index a5c98467..0c97219d 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -3,16 +3,15 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { User } from './user.entity'; -import { JwtStrategy } from '../auth/jwt.strategy'; import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; -import { AuthService } from '../auth/auth.service'; -import { Assignments } from '../volunteerAssignments/volunteerAssignments.entity'; +import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; import { Pantry } from '../pantries/pantries.entity'; +import { AuthService } from '../auth/auth.service'; @Module({ - imports: [TypeOrmModule.forFeature([User, Assignments, Pantry])], - exports: [UsersService, TypeOrmModule], + imports: [TypeOrmModule.forFeature([User, VolunteerAssignment, Pantry])], + exports: [UsersService], controllers: [UsersController], - providers: [UsersService, AuthService, JwtStrategy, CurrentUserInterceptor], + providers: [UsersService, AuthService, CurrentUserInterceptor], }) export class UsersModule {} diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 8388a79a..a4f348a7 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -8,8 +8,12 @@ import { Role } from './types'; import { mock } from 'jest-mock-extended'; import { In } from 'typeorm'; import { BadRequestException } from '@nestjs/common'; +import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; +import { Pantry } from '../pantries/pantries.entity'; const mockUserRepository = mock>(); +const mockAssignmentsRepository = mock>(); +const mockPantryRepository = mock>(); const mockUser: User = { id: 1, @@ -24,6 +28,16 @@ describe('UsersService', () => { let service: UsersService; beforeAll(async () => { + mockUserRepository.create.mockReset(); + mockUserRepository.save.mockReset(); + mockUserRepository.findOneBy.mockReset(); + mockUserRepository.find.mockReset(); + mockUserRepository.remove.mockReset(); + mockAssignmentsRepository.find.mockReset(); + mockAssignmentsRepository.save.mockReset(); + mockAssignmentsRepository.create.mockReset(); + mockPantryRepository.findBy.mockReset(); + const module = await Test.createTestingModule({ providers: [ UsersService, @@ -31,6 +45,14 @@ describe('UsersService', () => { provide: getRepositoryToken(User), useValue: mockUserRepository, }, + { + provide: getRepositoryToken(VolunteerAssignment), + useValue: mockAssignmentsRepository, + }, + { + provide: getRepositoryToken(Pantry), + useValue: mockPantryRepository, + }, ], }).compile(); @@ -43,6 +65,10 @@ describe('UsersService', () => { mockUserRepository.findOneBy.mockReset(); mockUserRepository.find.mockReset(); mockUserRepository.remove.mockReset(); + mockAssignmentsRepository.find.mockReset(); + mockAssignmentsRepository.save.mockReset(); + mockAssignmentsRepository.create.mockReset(); + mockPantryRepository.findBy.mockReset(); }); afterEach(() => { diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index bef91385..e81fe1d2 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -9,7 +9,7 @@ import { In, Repository } from 'typeorm'; import { User } from './user.entity'; import { Role, VOLUNTEER_ROLES } from './types'; import { validateId } from '../utils/validation.utils'; -import { Assignments } from '../volunteerAssignments/volunteerAssignments.entity'; +import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; import { Pantry } from '../pantries/pantries.entity'; @Injectable() @@ -18,8 +18,8 @@ export class UsersService { @InjectRepository(User) private repo: Repository, - @InjectRepository(Assignments) - private assignmentsRepo: Repository, + @InjectRepository(VolunteerAssignment) + private assignmentsRepo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, @@ -103,28 +103,30 @@ export class UsersService { }); } - async getVolunteerPantries(volunteerId: number) { + async getVolunteerPantries(volunteerId: number): Promise { validateId(volunteerId, 'Volunteer'); + + const volunteer = await this.repo.findOne({ where: { id: volunteerId } }); + if (!volunteer) + throw new NotFoundException(`Volunteer ${volunteerId} not found`); + const assignments = await this.assignmentsRepo.find({ - where: { volunteer: { id: volunteerId } }, + where: { volunteer: volunteer}, relations: ['pantry'], }); return assignments.map((a) => a.pantry); } - async assignPantriesToVolunteer(volunteerId: number, pantryIds: number[]) { + async assignPantriesToVolunteer(volunteerId: number, pantryIds: number[]): Promise { validateId(volunteerId, 'Volunteer'); - for (const pantryId of pantryIds) { - validateId(pantryId, 'Pantry'); - } + pantryIds.forEach((id) => validateId(id, 'Pantry')); const volunteer = await this.repo.findOne({ where: { id: volunteerId } }); if (!volunteer) throw new NotFoundException(`Volunteer ${volunteerId} not found`); const pantries = await this.pantryRepo.findBy({ pantryId: In(pantryIds) }); - if (pantries.length !== pantryIds.length) { throw new BadRequestException('One or more pantries not found'); } diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.controller.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.controller.ts index 84912f9f..93cc638a 100644 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.controller.ts +++ b/apps/backend/src/volunteerAssignments/volunteerAssignments.controller.ts @@ -1,13 +1,13 @@ import { Controller, Get } from '@nestjs/common'; import { AssignmentsService } from './volunteerAssignments.service'; -import { Assignments } from './volunteerAssignments.entity'; +import { VolunteerAssignment } from './volunteerAssignments.entity'; @Controller('assignments') export class AssignmentsController { constructor(private assignmentsService: AssignmentsService) {} - @Get('') - async getAssignments(): Promise { + @Get('/') + async getAssignments(): Promise { return this.assignmentsService.getAssignments(); } } diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts index 9d7bca73..0693b260 100644 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts +++ b/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts @@ -3,7 +3,7 @@ import { User } from '../users/user.entity'; import { Pantry } from '../pantries/pantries.entity'; @Entity('volunteer_assignments') -export class Assignments { +export class VolunteerAssignment { @PrimaryColumn({ name: 'volunteer_id' }) volunteerId: number; diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.module.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.module.ts index dc0382e8..42e2da98 100644 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.module.ts +++ b/apps/backend/src/volunteerAssignments/volunteerAssignments.module.ts @@ -2,13 +2,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { JwtStrategy } from '../auth/jwt.strategy'; import { AuthService } from '../auth/auth.service'; -import { Assignments } from './volunteerAssignments.entity'; +import { VolunteerAssignment } from './volunteerAssignments.entity'; import { AssignmentsController } from './volunteerAssignments.controller'; import { AssignmentsService } from './volunteerAssignments.service'; -import { UsersModule } from '../users/users.module'; @Module({ - imports: [TypeOrmModule.forFeature([Assignments]), UsersModule], + imports: [TypeOrmModule.forFeature([VolunteerAssignment])], controllers: [AssignmentsController], providers: [AssignmentsService, AuthService, JwtStrategy], }) diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts index d20fff30..a7a4e4b2 100644 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts +++ b/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts @@ -1,14 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { Assignments } from './volunteerAssignments.entity'; -import { UsersService } from '../users/users.service'; +import { VolunteerAssignment } from './volunteerAssignments.entity'; @Injectable() export class AssignmentsService { constructor( - @InjectRepository(Assignments) private repo: Repository, - private usersService: UsersService, + @InjectRepository(VolunteerAssignment) private repo: Repository, ) {} // Gets the volunteer details and the corresponding pantry diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 6c179b8e..31f90876 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -174,18 +174,10 @@ export enum VolunteerType { } export interface VolunteerPantryAssignment { - volunteer: { - id: number; - firstName: string; - lastName: string; - email: string; - phone: string; - role: string; - }; - pantry: { - pantryId: number; - pantryName: string; - }; + volunteerId: number; + pantryId: number; + volunteer: User; + pantry: Pantry; } export enum Role { From 297797d8221cd97c5088877ac42ae72549548a11 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:42:29 -0500 Subject: [PATCH 04/18] prettier --- apps/backend/src/pantries/pantries.controller.ts | 1 - apps/backend/src/users/users.service.ts | 7 +++++-- .../volunteerAssignments/volunteerAssignments.service.ts | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index 0bcd6484..ee8287ce 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -204,7 +204,6 @@ export class PantriesController { ], }, }) - @Post() async submitPantryApplication( @Body(new ValidationPipe()) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index e81fe1d2..84a4afc2 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -111,14 +111,17 @@ export class UsersService { throw new NotFoundException(`Volunteer ${volunteerId} not found`); const assignments = await this.assignmentsRepo.find({ - where: { volunteer: volunteer}, + where: { volunteer: volunteer }, relations: ['pantry'], }); return assignments.map((a) => a.pantry); } - async assignPantriesToVolunteer(volunteerId: number, pantryIds: number[]): Promise { + async assignPantriesToVolunteer( + volunteerId: number, + pantryIds: number[], + ): Promise { validateId(volunteerId, 'Volunteer'); pantryIds.forEach((id) => validateId(id, 'Pantry')); diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts index a7a4e4b2..e20b71f3 100644 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts +++ b/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts @@ -6,7 +6,8 @@ import { VolunteerAssignment } from './volunteerAssignments.entity'; @Injectable() export class AssignmentsService { constructor( - @InjectRepository(VolunteerAssignment) private repo: Repository, + @InjectRepository(VolunteerAssignment) + private repo: Repository, ) {} // Gets the volunteer details and the corresponding pantry From 1bea9388281728cd4a358654a8328331ad02efac Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:46:00 -0500 Subject: [PATCH 05/18] add tests and prettier --- .../donationItems/donationItems.controller.ts | 9 +- .../src/donations/donations.controller.ts | 4 +- apps/backend/src/donations/types.ts | 2 +- .../src/foodRequests/request.controller.ts | 9 +- ...1763963056712-AllergyFriendlyToBoolType.ts | 20 +- apps/backend/src/orders/order.service.spec.ts | 8 +- apps/backend/src/pantries/types.ts | 2 +- .../src/users/users.controller.spec.ts | 175 ++++++++++++++++-- apps/backend/src/users/users.controller.ts | 8 +- apps/frontend/src/types/pantryEnums.ts | 4 +- apps/frontend/src/types/types.ts | 11 +- 11 files changed, 203 insertions(+), 49 deletions(-) diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index 96381fc0..96be5673 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -38,8 +38,8 @@ export class DonationItemsController { status: { type: 'string', example: 'available' }, ozPerItem: { type: 'integer', example: 5 }, estimatedValue: { type: 'integer', example: 100 }, - foodType: { - type: 'string', + foodType: { + type: 'string', enum: Object.values(FoodType), example: FoodType.DAIRY_FREE_ALTERNATIVES, }, @@ -59,7 +59,10 @@ export class DonationItemsController { foodType: FoodType; }, ): Promise { - if (body.foodType && !Object.values(FoodType).includes(body.foodType as FoodType)) { + if ( + body.foodType && + !Object.values(FoodType).includes(body.foodType as FoodType) + ) { throw new BadRequestException('Invalid foodtype'); } return this.donationItemsService.create( diff --git a/apps/backend/src/donations/donations.controller.ts b/apps/backend/src/donations/donations.controller.ts index d748df66..6bcd2a7e 100644 --- a/apps/backend/src/donations/donations.controller.ts +++ b/apps/backend/src/donations/donations.controller.ts @@ -46,8 +46,8 @@ export class DonationsController { type: 'string', format: 'date-time', }, - status: { - type: 'string', + status: { + type: 'string', enum: Object.values(DonationStatus), example: DonationStatus.AVAILABLE, }, diff --git a/apps/backend/src/donations/types.ts b/apps/backend/src/donations/types.ts index 549ee6c6..16387987 100644 --- a/apps/backend/src/donations/types.ts +++ b/apps/backend/src/donations/types.ts @@ -2,4 +2,4 @@ export enum DonationStatus { AVAILABLE = 'available', FULFILLED = 'fulfilled', MATCHING = 'matching', -} \ No newline at end of file +} diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index c01eda5b..e3a93727 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -58,8 +58,8 @@ export class FoodRequestsController { type: 'object', properties: { pantryId: { type: 'integer', example: 1 }, - requestedSize: { - type: 'string', + requestedSize: { + type: 'string', enum: Object.values(RequestSize), example: RequestSize.LARGE, }, @@ -166,7 +166,10 @@ export class FoodRequestsController { ); const request = await this.requestsService.findOne(requestId); - await this.ordersService.updateStatus(request.order.orderId, OrderStatus.DELIVERED); + await this.ordersService.updateStatus( + request.order.orderId, + OrderStatus.DELIVERED, + ); return this.requestsService.updateDeliveryDetails( requestId, diff --git a/apps/backend/src/migrations/1763963056712-AllergyFriendlyToBoolType.ts b/apps/backend/src/migrations/1763963056712-AllergyFriendlyToBoolType.ts index 079362d6..3a14c4d4 100644 --- a/apps/backend/src/migrations/1763963056712-AllergyFriendlyToBoolType.ts +++ b/apps/backend/src/migrations/1763963056712-AllergyFriendlyToBoolType.ts @@ -1,21 +1,21 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; -export class AllergyFriendlyToBoolType1763963056712 implements MigrationInterface { - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` +export class AllergyFriendlyToBoolType1763963056712 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` ALTER TABLE pantries ALTER COLUMN dedicated_allergy_friendly TYPE BOOLEAN USING (FALSE), ALTER COLUMN dedicated_allergy_friendly SET NOT NULL; `); - } + } - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` ALTER TABLE pantries ALTER COLUMN dedicated_allergy_friendly TYPE VARCHAR(255), ALTER COLUMN dedicated_allergy_friendly DROP NOT NULL; `); - } - + } } diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 4c64ac10..b65e335b 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -6,7 +6,13 @@ import { OrdersService } from './order.service'; import { mock } from 'jest-mock-extended'; import { Pantry } from '../pantries/pantries.entity'; import { User } from '../users/user.entity'; -import { AllergensConfidence, ClientVisitFrequency, PantryStatus, RefrigeratedDonation, ServeAllergicChildren } from '../pantries/types'; +import { + AllergensConfidence, + ClientVisitFrequency, + PantryStatus, + RefrigeratedDonation, + ServeAllergicChildren, +} from '../pantries/types'; import { OrderStatus } from './types'; const mockOrdersRepository = mock>(); diff --git a/apps/backend/src/pantries/types.ts b/apps/backend/src/pantries/types.ts index f776991b..cdf8b671 100644 --- a/apps/backend/src/pantries/types.ts +++ b/apps/backend/src/pantries/types.ts @@ -33,7 +33,7 @@ export enum PantryStatus { export enum Activity { CREATE_LABELED_SHELF = 'Create labeled shelf', PROVIDE_EDUCATIONAL_PAMPHLETS = 'Provide educational pamphlets', - TRACK_DIETARY_NEEDS ='Spreadsheet to track dietary needs', + TRACK_DIETARY_NEEDS = 'Spreadsheet to track dietary needs', POST_RESOURCE_FLYERS = 'Post allergen-free resource flyers', SURVEY_CLIENTS = 'Survey clients to determine medical dietary needs', COLLECT_FEEDBACK = 'Collect feedback from allergen-avoidant clients', diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 78d116a3..a0ba0c09 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -7,6 +7,8 @@ import { userSchemaDto } from './dtos/userSchema.dto'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; +import { Pantry } from '../pantries/pantries.entity'; +import { Activity, PantryStatus, RefrigeratedDonation } from '../pantries/types'; const mockUserService = mock(); @@ -28,6 +30,85 @@ const mockUser2: User = { role: Role.LEAD_VOLUNTEER, }; +const mockUser3: User = { + id: 3, + email: 'test@test.com', + firstName: 'Test', + lastName: 'User', + phone: '5555555555', + role: Role.STANDARD_VOLUNTEER, +}; + +const mockPantries: Pantry[] = [ + { + pantryId: 1, + pantryName: 'Pantry A', + addressLine1: '123 Main St', + addressCity: 'City', + addressState: 'State', + addressZip: '12345', + allergenClients: 'Daily', + refrigeratedDonation: RefrigeratedDonation.NO, + reserveFoodForAllergic: 'Yes', + dedicatedAllergyFriendly: false, + newsletterSubscription: false, + restrictions: ['Egg allergy'], + pantryUser: mockUser1, + status: PantryStatus.PENDING, + dateApplied: new Date(), + activities: [ + Activity.COLLECT_FEEDBACK, + Activity.POST_RESOURCE_FLYERS, + ], + itemsInStock: 'bread', + needMoreOptions: 'No', + }, + { + pantryId: 2, + pantryName: 'Pantry B', + addressLine1: '456 Side St', + addressCity: 'Town', + addressState: 'Province', + addressZip: '67890', + allergenClients: 'Weekly', + refrigeratedDonation: RefrigeratedDonation.SOMETIMES, + reserveFoodForAllergic: 'No', + dedicatedAllergyFriendly: true, + newsletterSubscription: true, + restrictions: ['Milk allergy'], + pantryUser: mockUser1, + status: PantryStatus.APPROVED, + dateApplied: new Date(), + activities: [ + Activity.SURVEY_CLIENTS, + ], + itemsInStock: 'fruits', + needMoreOptions: 'Yes', + }, + { + pantryId: 3, + pantryName: 'Pantry C', + addressLine1: 'Address', + addressCity: 'City', + addressState: 'State', + addressZip: '10001', + allergenClients: 'Weekly', + refrigeratedDonation: RefrigeratedDonation.YES, + reserveFoodForAllergic: 'No', + dedicatedAllergyFriendly: false, + newsletterSubscription: true, + restrictions: ['Milk allergy'], + pantryUser: mockUser2, + status: PantryStatus.PENDING, + dateApplied: new Date(), + activities: [ + Activity.PROVIDE_EDUCATIONAL_PAMPHLETS, + ], + itemsInStock: 'fruits', + needMoreOptions: 'Yes', + }, +]; + describe('UsersController', () => { let controller: UsersController; @@ -55,24 +136,6 @@ describe('UsersController', () => { expect(controller).toBeDefined(); }); - describe('GET /volunteers', () => { - it('should return all volunteers', async () => { - const volunteers = [mockUser1, mockUser2]; - mockUserService.findUsersByRoles.mockResolvedValue(volunteers); - - const result = await controller.getAllVolunteers(); - - const hasAdmin = result.some((user) => user.role === Role.ADMIN); - expect(hasAdmin).toBe(false); - - expect(result).toEqual(volunteers); - expect(mockUserService.findUsersByRoles).toHaveBeenCalledWith([ - Role.LEAD_VOLUNTEER, - Role.STANDARD_VOLUNTEER, - ]); - }); - }); - describe('GET /:id', () => { it('should return a user by id', async () => { mockUserService.findOne.mockResolvedValue(mockUser1); @@ -158,4 +221,80 @@ describe('UsersController', () => { ); }); }); + + describe('GET /volunteers', () => { + it('should return all volunteers with their pantry assignments', async () => { + const assignments = [ + { ...mockUser1, pantryIds: [1, 2] }, + { ...mockUser2, pantryIds: [1] }, + { ...mockUser3, pantryIds: [] }, + ]; + + mockUserService.getVolunteersAndPantryAssignments.mockResolvedValue( + assignments, + ); + + const result = await controller.getAllVolunteers(); + + const hasAdmin = result.some((user) => user.role === Role.ADMIN); + expect(hasAdmin).toBe(false); + + expect(result).toEqual(assignments); + expect(result).toHaveLength(3); + expect(result[0].id).toBe(1); + expect(result[0].pantryIds).toEqual([1, 2]); + expect(result[1].id).toBe(2543210); + expect(result[1].pantryIds).toEqual([1]); + expect(result[2].id).toBe(3); + expect(result[2].pantryIds).toEqual([]); + expect( + mockUserService.getVolunteersAndPantryAssignments, + ).toHaveBeenCalled(); + }); + }); + + describe('GET /:id/pantries', () => { + it('should return pantries assigned to a user', async () => { + mockUserService.getVolunteerPantries.mockResolvedValue( + mockPantries.slice(0, 2), + ); + + const result = await controller.getVolunteerPantries(1); + + expect(result).toHaveLength(2); + expect(result).toEqual(mockPantries.slice(0, 2)); + expect(mockUserService.getVolunteerPantries).toHaveBeenCalledWith(1); + }); + }); + + describe('POST /:id/pantries', () => { + it('should assign pantries to a volunteer and return result', async () => { + const pantryIds = [1, 3]; + const mockAssignments = [ + { + volunteerId: 3, + pantryId: 1, + volunteer: mockUser3, + pantry: mockPantries[0], + }, + { + volunteerId: 3, + pantryId: 3, + volunteer: mockUser3, + pantry: mockPantries[2], + }, + ]; + mockUserService.assignPantriesToVolunteer.mockResolvedValue( + mockAssignments, + ); + + const result = await controller.assignPantries(3, pantryIds); + + expect(result).toEqual(mockAssignments); + expect(mockUserService.assignPantriesToVolunteer).toHaveBeenCalledWith( + 3, + pantryIds, + ); + }); + }); }); diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index c24751ce..c02563b6 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -16,6 +16,8 @@ import { UsersService } from './users.service'; import { User } from './user.entity'; import { Role } from './types'; import { userSchemaDto } from './dtos/userSchema.dto'; +import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; +import { Pantry } from '../pantries/pantries.entity'; //import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; @Controller('users') @@ -35,7 +37,9 @@ export class UsersController { } @Get('/:id/pantries') - async getVolunteerPantries(@Param('id', ParseIntPipe) id: number) { + async getVolunteerPantries( + @Param('id', ParseIntPipe) id: number, + ): Promise { return this.usersService.getVolunteerPantries(id); } @@ -65,7 +69,7 @@ export class UsersController { async assignPantries( @Param('id', ParseIntPipe) id: number, @Body('pantryIds') pantryIds: number[], - ) { + ): Promise { return this.usersService.assignPantriesToVolunteer(id, pantryIds); } } diff --git a/apps/frontend/src/types/pantryEnums.ts b/apps/frontend/src/types/pantryEnums.ts index e5f13a6f..cdf8b671 100644 --- a/apps/frontend/src/types/pantryEnums.ts +++ b/apps/frontend/src/types/pantryEnums.ts @@ -33,7 +33,7 @@ export enum PantryStatus { export enum Activity { CREATE_LABELED_SHELF = 'Create labeled shelf', PROVIDE_EDUCATIONAL_PAMPHLETS = 'Provide educational pamphlets', - TRACK_DIETARY_NEEDS ='Spreadsheet to track dietary needs', + TRACK_DIETARY_NEEDS = 'Spreadsheet to track dietary needs', POST_RESOURCE_FLYERS = 'Post allergen-free resource flyers', SURVEY_CLIENTS = 'Survey clients to determine medical dietary needs', COLLECT_FEEDBACK = 'Collect feedback from allergen-avoidant clients', @@ -44,4 +44,4 @@ export enum ReserveFoodForAllergic { YES = 'Yes', SOME = 'Some', NO = 'No', -} \ No newline at end of file +} diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 31f90876..ce2438d3 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -1,12 +1,12 @@ -import { - RefrigeratedDonation, - ReserveFoodForAllergic, - ClientVisitFrequency, +import { + RefrigeratedDonation, + ReserveFoodForAllergic, + ClientVisitFrequency, ServeAllergicChildren, AllergensConfidence, PantryStatus, Activity, -} from "./pantryEnums"; +} from './pantryEnums'; // Note: The API calls as currently written do not // return a pantry's SSF representative or pantry @@ -214,4 +214,3 @@ export enum DonationStatus { FULFILLED = 'fulfilled', MATCHING = 'matching', } - From 50b174f5cce8e06053267de1dab38145ea477402 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Wed, 26 Nov 2025 12:46:13 -0500 Subject: [PATCH 06/18] prettier --- .../src/users/users.controller.spec.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index a0ba0c09..9f5a9c2c 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -8,7 +8,11 @@ import { userSchemaDto } from './dtos/userSchema.dto'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; import { Pantry } from '../pantries/pantries.entity'; -import { Activity, PantryStatus, RefrigeratedDonation } from '../pantries/types'; +import { + Activity, + PantryStatus, + RefrigeratedDonation, +} from '../pantries/types'; const mockUserService = mock(); @@ -56,10 +60,7 @@ const mockPantries: Pantry[] = [ pantryUser: mockUser1, status: PantryStatus.PENDING, dateApplied: new Date(), - activities: [ - Activity.COLLECT_FEEDBACK, - Activity.POST_RESOURCE_FLYERS, - ], + activities: [Activity.COLLECT_FEEDBACK, Activity.POST_RESOURCE_FLYERS], itemsInStock: 'bread', needMoreOptions: 'No', }, @@ -79,9 +80,7 @@ const mockPantries: Pantry[] = [ pantryUser: mockUser1, status: PantryStatus.APPROVED, dateApplied: new Date(), - activities: [ - Activity.SURVEY_CLIENTS, - ], + activities: [Activity.SURVEY_CLIENTS], itemsInStock: 'fruits', needMoreOptions: 'Yes', }, @@ -101,9 +100,7 @@ const mockPantries: Pantry[] = [ pantryUser: mockUser2, status: PantryStatus.PENDING, dateApplied: new Date(), - activities: [ - Activity.PROVIDE_EDUCATIONAL_PAMPHLETS, - ], + activities: [Activity.PROVIDE_EDUCATIONAL_PAMPHLETS], itemsInStock: 'fruits', needMoreOptions: 'Yes', }, From 1a74893ed09b0c6a8be61a2691fb078bc6b28ea4 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:47:47 -0500 Subject: [PATCH 07/18] review comments --- .../src/users/users.controller.spec.ts | 101 ++++-------------- apps/backend/src/users/users.controller.ts | 2 +- apps/backend/src/users/users.module.ts | 3 +- apps/backend/src/users/users.service.ts | 10 +- 4 files changed, 26 insertions(+), 90 deletions(-) diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 9f5a9c2c..5a3a35b1 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -8,101 +8,36 @@ import { userSchemaDto } from './dtos/userSchema.dto'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; import { Pantry } from '../pantries/pantries.entity'; -import { - Activity, - PantryStatus, - RefrigeratedDonation, -} from '../pantries/types'; const mockUserService = mock(); -const mockUser1: User = { +const mockUser1: Partial = { id: 1, - email: 'john@example.com', - firstName: 'John', - lastName: 'Doe', - phone: '1234567890', role: Role.STANDARD_VOLUNTEER, }; -const mockUser2: User = { +const mockUser2: Partial = { id: 2543210, - email: 'bobsmith@example.com', - firstName: 'Bob', - lastName: 'Smith', - phone: '9876', role: Role.LEAD_VOLUNTEER, }; -const mockUser3: User = { +const mockUser3: Partial = { id: 3, - email: 'test@test.com', - firstName: 'Test', - lastName: 'User', - phone: '5555555555', role: Role.STANDARD_VOLUNTEER, }; -const mockPantries: Pantry[] = [ +const mockPantries: Partial[] = [ { pantryId: 1, - pantryName: 'Pantry A', - addressLine1: '123 Main St', - addressCity: 'City', - addressState: 'State', - addressZip: '12345', - allergenClients: 'Daily', - refrigeratedDonation: RefrigeratedDonation.NO, - reserveFoodForAllergic: 'Yes', - dedicatedAllergyFriendly: false, - newsletterSubscription: false, - restrictions: ['Egg allergy'], - pantryUser: mockUser1, - status: PantryStatus.PENDING, - dateApplied: new Date(), - activities: [Activity.COLLECT_FEEDBACK, Activity.POST_RESOURCE_FLYERS], - itemsInStock: 'bread', - needMoreOptions: 'No', + pantryUser: mockUser1 as User, }, { pantryId: 2, - pantryName: 'Pantry B', - addressLine1: '456 Side St', - addressCity: 'Town', - addressState: 'Province', - addressZip: '67890', - allergenClients: 'Weekly', - refrigeratedDonation: RefrigeratedDonation.SOMETIMES, - reserveFoodForAllergic: 'No', - dedicatedAllergyFriendly: true, - newsletterSubscription: true, - restrictions: ['Milk allergy'], - pantryUser: mockUser1, - status: PantryStatus.APPROVED, - dateApplied: new Date(), - activities: [Activity.SURVEY_CLIENTS], - itemsInStock: 'fruits', - needMoreOptions: 'Yes', + pantryUser: mockUser1 as User, }, { pantryId: 3, - pantryName: 'Pantry C', - addressLine1: 'Address', - addressCity: 'City', - addressState: 'State', - addressZip: '10001', - allergenClients: 'Weekly', - refrigeratedDonation: RefrigeratedDonation.YES, - reserveFoodForAllergic: 'No', - dedicatedAllergyFriendly: false, - newsletterSubscription: true, - restrictions: ['Milk allergy'], - pantryUser: mockUser2, - status: PantryStatus.PENDING, - dateApplied: new Date(), - activities: [Activity.PROVIDE_EDUCATIONAL_PAMPHLETS], - itemsInStock: 'fruits', - needMoreOptions: 'Yes', + pantryUser: mockUser2 as User, }, ]; @@ -135,7 +70,7 @@ describe('UsersController', () => { describe('GET /:id', () => { it('should return a user by id', async () => { - mockUserService.findOne.mockResolvedValue(mockUser1); + mockUserService.findOne.mockResolvedValue(mockUser1 as User); const result = await controller.getUser(1); @@ -146,7 +81,7 @@ describe('UsersController', () => { describe('DELETE /:id', () => { it('should remove a user by id', async () => { - mockUserService.remove.mockResolvedValue(mockUser1); + mockUserService.remove.mockResolvedValue(mockUser1 as User); const result = await controller.removeUser(1); @@ -158,7 +93,7 @@ describe('UsersController', () => { describe('PUT :id/role', () => { it('should update user role with valid role', async () => { const updatedUser = { ...mockUser1, role: Role.ADMIN }; - mockUserService.update.mockResolvedValue(updatedUser); + mockUserService.update.mockResolvedValue(updatedUser as User); const result = await controller.updateRole(1, Role.ADMIN); @@ -222,9 +157,9 @@ describe('UsersController', () => { describe('GET /volunteers', () => { it('should return all volunteers with their pantry assignments', async () => { const assignments = [ - { ...mockUser1, pantryIds: [1, 2] }, - { ...mockUser2, pantryIds: [1] }, - { ...mockUser3, pantryIds: [] }, + { ...(mockUser1 as User), pantryIds: [1, 2] }, + { ...(mockUser2 as User), pantryIds: [1] }, + { ...(mockUser3 as User), pantryIds: [] }, ]; mockUserService.getVolunteersAndPantryAssignments.mockResolvedValue( @@ -253,7 +188,7 @@ describe('UsersController', () => { describe('GET /:id/pantries', () => { it('should return pantries assigned to a user', async () => { mockUserService.getVolunteerPantries.mockResolvedValue( - mockPantries.slice(0, 2), + mockPantries.slice(0, 2) as Pantry[], ); const result = await controller.getVolunteerPantries(1); @@ -271,14 +206,14 @@ describe('UsersController', () => { { volunteerId: 3, pantryId: 1, - volunteer: mockUser3, - pantry: mockPantries[0], + volunteer: mockUser3 as User, + pantry: mockPantries[0] as Pantry, }, { volunteerId: 3, pantryId: 3, - volunteer: mockUser3, - pantry: mockPantries[2], + volunteer: mockUser3 as User, + pantry: mockPantries[2] as Pantry, }, ]; mockUserService.assignPantriesToVolunteer.mockResolvedValue( diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index c02563b6..d2d6fefb 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -26,7 +26,7 @@ export class UsersController { constructor(private usersService: UsersService) {} @Get('/volunteers') - async getAllVolunteers() { + async getAllVolunteers(): Promise<(User & { pantryIds: number[] })[]> { return this.usersService.getVolunteersAndPantryAssignments(); } diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index 0c97219d..c74d8210 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { User } from './user.entity'; +import { JwtStrategy } from '../auth/jwt.strategy'; import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; import { Pantry } from '../pantries/pantries.entity'; @@ -12,6 +13,6 @@ import { AuthService } from '../auth/auth.service'; imports: [TypeOrmModule.forFeature([User, VolunteerAssignment, Pantry])], exports: [UsersService], controllers: [UsersController], - providers: [UsersService, AuthService, CurrentUserInterceptor], + providers: [UsersService, AuthService, JwtStrategy, CurrentUserInterceptor], }) export class UsersModule {} diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 84a4afc2..60df3a56 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -19,7 +19,7 @@ export class UsersService { private repo: Repository, @InjectRepository(VolunteerAssignment) - private assignmentsRepo: Repository, + private volunteerAssignmentsRepo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, @@ -91,7 +91,7 @@ export class UsersService { async getVolunteersAndPantryAssignments() { const volunteers = await this.findUsersByRoles(VOLUNTEER_ROLES); - const assignments = await this.assignmentsRepo.find({ + const assignments = await this.volunteerAssignmentsRepo.find({ relations: ['pantry', 'volunteer'], }); @@ -110,7 +110,7 @@ export class UsersService { if (!volunteer) throw new NotFoundException(`Volunteer ${volunteerId} not found`); - const assignments = await this.assignmentsRepo.find({ + const assignments = await this.volunteerAssignmentsRepo.find({ where: { volunteer: volunteer }, relations: ['pantry'], }); @@ -135,9 +135,9 @@ export class UsersService { } const assignments = pantries.map((pantry) => - this.assignmentsRepo.create({ volunteer, pantry }), + this.volunteerAssignmentsRepo.create({ volunteer, pantry }), ); - return this.assignmentsRepo.save(assignments); + return this.volunteerAssignmentsRepo.save(assignments); } } From 38b0641216df2cfc59ef2b53dec2c772af335935 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:26:51 -0500 Subject: [PATCH 08/18] review comments --- apps/backend/src/users/users.controller.spec.ts | 5 +++-- apps/backend/src/users/users.service.spec.ts | 17 +++++++++-------- apps/backend/src/users/users.service.ts | 14 ++++++++++---- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 5a3a35b1..98061968 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -8,6 +8,7 @@ import { userSchemaDto } from './dtos/userSchema.dto'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; import { Pantry } from '../pantries/pantries.entity'; +import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; const mockUserService = mock(); @@ -156,7 +157,7 @@ describe('UsersController', () => { describe('GET /volunteers', () => { it('should return all volunteers with their pantry assignments', async () => { - const assignments = [ + const assignments: (User & { pantryIds: number[] })[] = [ { ...(mockUser1 as User), pantryIds: [1, 2] }, { ...(mockUser2 as User), pantryIds: [1] }, { ...(mockUser3 as User), pantryIds: [] }, @@ -202,7 +203,7 @@ describe('UsersController', () => { describe('POST /:id/pantries', () => { it('should assign pantries to a volunteer and return result', async () => { const pantryIds = [1, 3]; - const mockAssignments = [ + const mockAssignments: VolunteerAssignment[] = [ { volunteerId: 3, pantryId: 1, diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index a4f348a7..0440d1da 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -12,7 +12,8 @@ import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignment import { Pantry } from '../pantries/pantries.entity'; const mockUserRepository = mock>(); -const mockAssignmentsRepository = mock>(); +const mockVolunteerAssignmentsRepository = + mock>(); const mockPantryRepository = mock>(); const mockUser: User = { @@ -33,9 +34,9 @@ describe('UsersService', () => { mockUserRepository.findOneBy.mockReset(); mockUserRepository.find.mockReset(); mockUserRepository.remove.mockReset(); - mockAssignmentsRepository.find.mockReset(); - mockAssignmentsRepository.save.mockReset(); - mockAssignmentsRepository.create.mockReset(); + mockVolunteerAssignmentsRepository.find.mockReset(); + mockVolunteerAssignmentsRepository.save.mockReset(); + mockVolunteerAssignmentsRepository.create.mockReset(); mockPantryRepository.findBy.mockReset(); const module = await Test.createTestingModule({ @@ -47,7 +48,7 @@ describe('UsersService', () => { }, { provide: getRepositoryToken(VolunteerAssignment), - useValue: mockAssignmentsRepository, + useValue: mockVolunteerAssignmentsRepository, }, { provide: getRepositoryToken(Pantry), @@ -65,9 +66,9 @@ describe('UsersService', () => { mockUserRepository.findOneBy.mockReset(); mockUserRepository.find.mockReset(); mockUserRepository.remove.mockReset(); - mockAssignmentsRepository.find.mockReset(); - mockAssignmentsRepository.save.mockReset(); - mockAssignmentsRepository.create.mockReset(); + mockVolunteerAssignmentsRepository.find.mockReset(); + mockVolunteerAssignmentsRepository.save.mockReset(); + mockVolunteerAssignmentsRepository.create.mockReset(); mockPantryRepository.findBy.mockReset(); }); diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 60df3a56..a843ab0e 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -125,9 +125,15 @@ export class UsersService { validateId(volunteerId, 'Volunteer'); pantryIds.forEach((id) => validateId(id, 'Pantry')); - const volunteer = await this.repo.findOne({ where: { id: volunteerId } }); - if (!volunteer) - throw new NotFoundException(`Volunteer ${volunteerId} not found`); + const user = await this.repo.findOne({ where: { id: volunteerId } }); + if (!user) { + throw new NotFoundException(`User ${volunteerId} not found`); + } + if (!VOLUNTEER_ROLES.includes(user.role)) { + throw new BadRequestException( + `User ${volunteerId} is not a volunteer and cannot be assigned to pantries`, + ); + } const pantries = await this.pantryRepo.findBy({ pantryId: In(pantryIds) }); if (pantries.length !== pantryIds.length) { @@ -135,7 +141,7 @@ export class UsersService { } const assignments = pantries.map((pantry) => - this.volunteerAssignmentsRepo.create({ volunteer, pantry }), + this.volunteerAssignmentsRepo.create({ volunteer: user, pantry }), ); return this.volunteerAssignmentsRepo.save(assignments); From 2db8cecd078022285ce8f3320ff9e4982fdd5cd5 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:28:18 -0500 Subject: [PATCH 09/18] change to singular repo --- apps/backend/src/users/users.service.spec.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 0440d1da..4f27c19c 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -12,7 +12,7 @@ import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignment import { Pantry } from '../pantries/pantries.entity'; const mockUserRepository = mock>(); -const mockVolunteerAssignmentsRepository = +const mockVolunteerAssignmentRepository = mock>(); const mockPantryRepository = mock>(); @@ -34,9 +34,9 @@ describe('UsersService', () => { mockUserRepository.findOneBy.mockReset(); mockUserRepository.find.mockReset(); mockUserRepository.remove.mockReset(); - mockVolunteerAssignmentsRepository.find.mockReset(); - mockVolunteerAssignmentsRepository.save.mockReset(); - mockVolunteerAssignmentsRepository.create.mockReset(); + mockVolunteerAssignmentRepository.find.mockReset(); + mockVolunteerAssignmentRepository.save.mockReset(); + mockVolunteerAssignmentRepository.create.mockReset(); mockPantryRepository.findBy.mockReset(); const module = await Test.createTestingModule({ @@ -48,7 +48,7 @@ describe('UsersService', () => { }, { provide: getRepositoryToken(VolunteerAssignment), - useValue: mockVolunteerAssignmentsRepository, + useValue: mockVolunteerAssignmentRepository, }, { provide: getRepositoryToken(Pantry), @@ -66,9 +66,9 @@ describe('UsersService', () => { mockUserRepository.findOneBy.mockReset(); mockUserRepository.find.mockReset(); mockUserRepository.remove.mockReset(); - mockVolunteerAssignmentsRepository.find.mockReset(); - mockVolunteerAssignmentsRepository.save.mockReset(); - mockVolunteerAssignmentsRepository.create.mockReset(); + mockVolunteerAssignmentRepository.find.mockReset(); + mockVolunteerAssignmentRepository.save.mockReset(); + mockVolunteerAssignmentRepository.create.mockReset(); mockPantryRepository.findBy.mockReset(); }); From 80381025ae04280d5504db4b66824daaac03c610 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:29:55 -0500 Subject: [PATCH 10/18] change to singular repo --- apps/backend/src/users/users.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index a843ab0e..097c5a85 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -19,7 +19,7 @@ export class UsersService { private repo: Repository, @InjectRepository(VolunteerAssignment) - private volunteerAssignmentsRepo: Repository, + private volunteerAssignmentRepo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, @@ -91,7 +91,7 @@ export class UsersService { async getVolunteersAndPantryAssignments() { const volunteers = await this.findUsersByRoles(VOLUNTEER_ROLES); - const assignments = await this.volunteerAssignmentsRepo.find({ + const assignments = await this.volunteerAssignmentRepo.find({ relations: ['pantry', 'volunteer'], }); @@ -110,7 +110,7 @@ export class UsersService { if (!volunteer) throw new NotFoundException(`Volunteer ${volunteerId} not found`); - const assignments = await this.volunteerAssignmentsRepo.find({ + const assignments = await this.volunteerAssignmentRepo.find({ where: { volunteer: volunteer }, relations: ['pantry'], }); @@ -141,9 +141,9 @@ export class UsersService { } const assignments = pantries.map((pantry) => - this.volunteerAssignmentsRepo.create({ volunteer: user, pantry }), + this.volunteerAssignmentRepo.create({ volunteer: user, pantry }), ); - return this.volunteerAssignmentsRepo.save(assignments); + return this.volunteerAssignmentRepo.save(assignments); } } From b2d26420ed3e1b10f0fe90169f259e9457511df5 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:57:37 -0500 Subject: [PATCH 11/18] remove unnecessary logic check --- apps/backend/src/users/users.controller.spec.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 98061968..46deec0f 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -169,9 +169,6 @@ describe('UsersController', () => { const result = await controller.getAllVolunteers(); - const hasAdmin = result.some((user) => user.role === Role.ADMIN); - expect(hasAdmin).toBe(false); - expect(result).toEqual(assignments); expect(result).toHaveLength(3); expect(result[0].id).toBe(1); From 32fb226dc334c72abde158a18289767d08c5cc3a Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sat, 29 Nov 2025 15:21:37 -0500 Subject: [PATCH 12/18] add many to many relation and remove direct usage of pantry repo --- apps/backend/src/app.module.ts | 2 - ...4668-AddVolunteerPantryUniqueConstraint.ts | 4 ++ apps/backend/src/orders/order.service.spec.ts | 1 + apps/backend/src/pantries/pantries.entity.ts | 4 ++ apps/backend/src/pantries/pantries.module.ts | 8 +-- apps/backend/src/pantries/pantries.service.ts | 18 ++++- apps/backend/src/users/dtos/userSchema.dto.ts | 1 - apps/backend/src/users/user.entity.ts | 23 ++++++- .../src/users/users.controller.spec.ts | 31 +++------ apps/backend/src/users/users.controller.ts | 7 +- apps/backend/src/users/users.module.ts | 5 +- apps/backend/src/users/users.service.spec.ts | 31 +++------ apps/backend/src/users/users.service.ts | 68 +++++++++---------- .../volunteerAssignments.controller.ts | 13 ---- .../volunteerAssignments.entity.ts | 26 ------- .../volunteerAssignments.module.ts | 14 ---- .../volunteerAssignments.service.ts | 34 ---------- 17 files changed, 110 insertions(+), 180 deletions(-) delete mode 100644 apps/backend/src/volunteerAssignments/volunteerAssignments.controller.ts delete mode 100644 apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts delete mode 100644 apps/backend/src/volunteerAssignments/volunteerAssignments.module.ts delete mode 100644 apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 5c0294a8..3c0ce87a 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -2,7 +2,6 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RequestsModule } from './foodRequests/request.module'; import { PantriesModule } from './pantries/pantries.module'; -import { AssignmentsModule } from './volunteerAssignments/volunteerAssignments.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UsersModule } from './users/users.module'; @@ -34,7 +33,6 @@ import { AllocationModule } from './allocations/allocations.module'; AuthModule, PantriesModule, RequestsModule, - AssignmentsModule, DonationModule, DonationItemsModule, OrdersModule, diff --git a/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts index 90e94be5..9794796a 100644 --- a/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts +++ b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts @@ -29,6 +29,10 @@ export class AddVolunteerPantryUniqueConstraint1760033134668 ALTER TABLE volunteer_assignments DROP CONSTRAINT volunteer_assignments_pkey; ALTER TABLE volunteer_assignments ADD COLUMN assignment_id SERIAL PRIMARY KEY; + + ALTER TABLE volunteer_assignments + ADD CONSTRAINT fk_volunteer_id FOREIGN KEY(volunteer_id) REFERENCES users(user_id); + ADD CONSTRAINT fk_pantry_id FOREIGN KEY(pantry_id) REFERENCES pantries(pantry_id); `); } } diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index b65e335b..69ef277b 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -43,6 +43,7 @@ const mockPantry: Pantry = { activitiesComments: '', itemsInStock: '', needMoreOptions: '', + volunteers: [], }; describe('OrdersService', () => { diff --git a/apps/backend/src/pantries/pantries.entity.ts b/apps/backend/src/pantries/pantries.entity.ts index 20ddacff..d714e587 100644 --- a/apps/backend/src/pantries/pantries.entity.ts +++ b/apps/backend/src/pantries/pantries.entity.ts @@ -4,6 +4,7 @@ import { PrimaryGeneratedColumn, OneToOne, JoinColumn, + ManyToMany, } from 'typeorm'; import { User } from '../users/user.entity'; import { @@ -158,4 +159,7 @@ export class Pantry { @Column({ name: 'need_more_options', type: 'text' }) needMoreOptions: string; + + @ManyToMany(() => User, (user) => user.pantries) + volunteers?: User[]; } diff --git a/apps/backend/src/pantries/pantries.module.ts b/apps/backend/src/pantries/pantries.module.ts index 5653396d..3de2a4c5 100644 --- a/apps/backend/src/pantries/pantries.module.ts +++ b/apps/backend/src/pantries/pantries.module.ts @@ -1,16 +1,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { User } from '../users/user.entity'; import { PantriesService } from './pantries.service'; import { PantriesController } from './pantries.controller'; -import { JwtStrategy } from '../auth/jwt.strategy'; -import { AuthService } from '../auth/auth.service'; import { Pantry } from './pantries.entity'; import { OrdersModule } from '../orders/order.module'; @Module({ - imports: [TypeOrmModule.forFeature([Pantry, User]), OrdersModule], + imports: [TypeOrmModule.forFeature([Pantry]), OrdersModule], controllers: [PantriesController], - providers: [PantriesService, AuthService, JwtStrategy], + providers: [PantriesService], + exports: [PantriesService], }) export class PantriesModule {} diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts index 2e10535b..788341bc 100644 --- a/apps/backend/src/pantries/pantries.service.ts +++ b/apps/backend/src/pantries/pantries.service.ts @@ -1,6 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { Pantry } from './pantries.entity'; import { User } from '../users/user.entity'; import { validateId } from '../utils/validation.utils'; @@ -90,4 +90,20 @@ export class PantriesService { await this.repo.update(id, { status: PantryStatus.DENIED }); } + + async findByIds(pantryIds: number[]): Promise { + pantryIds.forEach((id) => validateId(id, 'Pantry')); + + const pantries = await this.repo.findBy({ pantryId: In(pantryIds) }); + + if (pantries.length !== pantryIds.length) { + const foundIds = pantries.map((p) => p.pantryId); + const missingIds = pantryIds.filter((id) => !foundIds.includes(id)); + throw new NotFoundException( + `Pantries not found: ${missingIds.join(', ')}`, + ); + } + + return pantries; + } } diff --git a/apps/backend/src/users/dtos/userSchema.dto.ts b/apps/backend/src/users/dtos/userSchema.dto.ts index b6905ea2..25cbc299 100644 --- a/apps/backend/src/users/dtos/userSchema.dto.ts +++ b/apps/backend/src/users/dtos/userSchema.dto.ts @@ -3,7 +3,6 @@ import { IsEnum, IsNotEmpty, IsString, - IsOptional, IsPhoneNumber, } from 'class-validator'; import { Role } from '../types'; diff --git a/apps/backend/src/users/user.entity.ts b/apps/backend/src/users/user.entity.ts index b0d95316..e2b9a958 100644 --- a/apps/backend/src/users/user.entity.ts +++ b/apps/backend/src/users/user.entity.ts @@ -1,6 +1,13 @@ -import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +import { + Entity, + Column, + PrimaryGeneratedColumn, + ManyToMany, + JoinTable, +} from 'typeorm'; import { Role } from './types'; +import { Pantry } from '../pantries/pantries.entity'; @Entity() export class User { @@ -30,4 +37,18 @@ export class User { length: 20, }) phone: string; + + @ManyToMany(() => Pantry, (pantry) => pantry.volunteers) + @JoinTable({ + name: 'volunteer_assignments', + joinColumn: { + name: 'volunteer_id', + referencedColumnName: 'id', + }, + inverseJoinColumn: { + name: 'pantry_id', + referencedColumnName: 'pantryId', + }, + }) + pantries?: Pantry[]; } diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 46deec0f..21c33026 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -8,7 +8,6 @@ import { userSchemaDto } from './dtos/userSchema.dto'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; import { Pantry } from '../pantries/pantries.entity'; -import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; const mockUserService = mock(); @@ -122,7 +121,7 @@ describe('UsersController', () => { role: Role.ADMIN, }; - const createdUser = { ...createUserSchema, id: 2 }; + const createdUser = { ...createUserSchema, id: 2 } as User; mockUserService.create.mockResolvedValue(createdUser); const result = await controller.createUser(createUserSchema); @@ -200,27 +199,19 @@ describe('UsersController', () => { describe('POST /:id/pantries', () => { it('should assign pantries to a volunteer and return result', async () => { const pantryIds = [1, 3]; - const mockAssignments: VolunteerAssignment[] = [ - { - volunteerId: 3, - pantryId: 1, - volunteer: mockUser3 as User, - pantry: mockPantries[0] as Pantry, - }, - { - volunteerId: 3, - pantryId: 3, - volunteer: mockUser3 as User, - pantry: mockPantries[2] as Pantry, - }, - ]; - mockUserService.assignPantriesToVolunteer.mockResolvedValue( - mockAssignments, - ); + const updatedUser = { + ...mockUser3, + pantries: [mockPantries[0] as Pantry, mockPantries[2] as Pantry], + } as User; + + mockUserService.assignPantriesToVolunteer.mockResolvedValue(updatedUser); const result = await controller.assignPantries(3, pantryIds); - expect(result).toEqual(mockAssignments); + expect(result).toEqual(updatedUser); + expect(result.pantries).toHaveLength(2); + expect(result.pantries[0].pantryId).toBe(1); + expect(result.pantries[1].pantryId).toBe(3); expect(mockUserService.assignPantriesToVolunteer).toHaveBeenCalledWith( 3, pantryIds, diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index d2d6fefb..6f11265d 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -16,7 +16,6 @@ import { UsersService } from './users.service'; import { User } from './user.entity'; import { Role } from './types'; import { userSchemaDto } from './dtos/userSchema.dto'; -import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; import { Pantry } from '../pantries/pantries.entity'; //import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; @@ -26,7 +25,9 @@ export class UsersController { constructor(private usersService: UsersService) {} @Get('/volunteers') - async getAllVolunteers(): Promise<(User & { pantryIds: number[] })[]> { + async getAllVolunteers(): Promise< + (Omit & { pantryIds: number[] })[] + > { return this.usersService.getVolunteersAndPantryAssignments(); } @@ -69,7 +70,7 @@ export class UsersController { async assignPantries( @Param('id', ParseIntPipe) id: number, @Body('pantryIds') pantryIds: number[], - ): Promise { + ): Promise { return this.usersService.assignPantriesToVolunteer(id, pantryIds); } } diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index c74d8210..6a780a8d 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -5,12 +5,11 @@ import { UsersService } from './users.service'; import { User } from './user.entity'; import { JwtStrategy } from '../auth/jwt.strategy'; import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; -import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; -import { Pantry } from '../pantries/pantries.entity'; import { AuthService } from '../auth/auth.service'; +import { PantriesModule } from '../pantries/pantries.module'; @Module({ - imports: [TypeOrmModule.forFeature([User, VolunteerAssignment, Pantry])], + imports: [TypeOrmModule.forFeature([User]), PantriesModule], exports: [UsersService], controllers: [UsersController], providers: [UsersService, AuthService, JwtStrategy, CurrentUserInterceptor], diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 4f27c19c..0cfcb539 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -8,22 +8,19 @@ import { Role } from './types'; import { mock } from 'jest-mock-extended'; import { In } from 'typeorm'; import { BadRequestException } from '@nestjs/common'; -import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; -import { Pantry } from '../pantries/pantries.entity'; +import { PantriesService } from '../pantries/pantries.service'; const mockUserRepository = mock>(); -const mockVolunteerAssignmentRepository = - mock>(); -const mockPantryRepository = mock>(); +const mockPantriesService = mock(); -const mockUser: User = { +const mockUser = { id: 1, email: 'test@example.com', firstName: 'John', lastName: 'Doe', phone: '1234567890', role: Role.STANDARD_VOLUNTEER, -}; +} as User; describe('UsersService', () => { let service: UsersService; @@ -34,10 +31,7 @@ describe('UsersService', () => { mockUserRepository.findOneBy.mockReset(); mockUserRepository.find.mockReset(); mockUserRepository.remove.mockReset(); - mockVolunteerAssignmentRepository.find.mockReset(); - mockVolunteerAssignmentRepository.save.mockReset(); - mockVolunteerAssignmentRepository.create.mockReset(); - mockPantryRepository.findBy.mockReset(); + mockPantriesService.findByIds.mockReset(); const module = await Test.createTestingModule({ providers: [ @@ -47,12 +41,8 @@ describe('UsersService', () => { useValue: mockUserRepository, }, { - provide: getRepositoryToken(VolunteerAssignment), - useValue: mockVolunteerAssignmentRepository, - }, - { - provide: getRepositoryToken(Pantry), - useValue: mockPantryRepository, + provide: PantriesService, + useValue: mockPantriesService, }, ], }).compile(); @@ -66,10 +56,7 @@ describe('UsersService', () => { mockUserRepository.findOneBy.mockReset(); mockUserRepository.find.mockReset(); mockUserRepository.remove.mockReset(); - mockVolunteerAssignmentRepository.find.mockReset(); - mockVolunteerAssignmentRepository.save.mockReset(); - mockVolunteerAssignmentRepository.create.mockReset(); - mockPantryRepository.findBy.mockReset(); + mockPantriesService.findByIds.mockReset(); }); afterEach(() => { @@ -88,7 +75,7 @@ describe('UsersService', () => { lastName: 'Smith', phone: '9876543210', role: Role.ADMIN, - }; + } as User; const createdUser = { ...userData, id: 1 }; mockUserRepository.create.mockReturnValue(createdUser); diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 097c5a85..85740d50 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -9,8 +9,8 @@ import { In, Repository } from 'typeorm'; import { User } from './user.entity'; import { Role, VOLUNTEER_ROLES } from './types'; import { validateId } from '../utils/validation.utils'; -import { VolunteerAssignment } from '../volunteerAssignments/volunteerAssignments.entity'; import { Pantry } from '../pantries/pantries.entity'; +import { PantriesService } from '../pantries/pantries.service'; @Injectable() export class UsersService { @@ -18,11 +18,7 @@ export class UsersService { @InjectRepository(User) private repo: Repository, - @InjectRepository(VolunteerAssignment) - private volunteerAssignmentRepo: Repository, - - @InjectRepository(Pantry) - private pantryRepo: Repository, + private pantriesService: PantriesService, ) {} async create( @@ -88,62 +84,64 @@ export class UsersService { return this.repo.find({ where: { role: In(roles) } }); } - async getVolunteersAndPantryAssignments() { - const volunteers = await this.findUsersByRoles(VOLUNTEER_ROLES); - - const assignments = await this.volunteerAssignmentRepo.find({ - relations: ['pantry', 'volunteer'], + async getVolunteersAndPantryAssignments(): Promise< + (Omit & { pantryIds: number[] })[] + > { + const volunteers = await this.repo.find({ + where: { role: In(VOLUNTEER_ROLES) }, + relations: ['pantries'], }); return volunteers.map((v) => { - const assigned = assignments - .filter((a) => a.volunteer.id == v.id) - .map((a) => a.pantry.pantryId); - return { ...v, pantryIds: assigned }; + const { pantries, ...volunteerWithoutPantries } = v; + return { + ...volunteerWithoutPantries, + pantryIds: pantries.map((p) => p.pantryId), + }; }); } async getVolunteerPantries(volunteerId: number): Promise { validateId(volunteerId, 'Volunteer'); - const volunteer = await this.repo.findOne({ where: { id: volunteerId } }); - if (!volunteer) - throw new NotFoundException(`Volunteer ${volunteerId} not found`); - - const assignments = await this.volunteerAssignmentRepo.find({ - where: { volunteer: volunteer }, - relations: ['pantry'], + const user = await this.repo.findOne({ + where: { id: volunteerId }, + relations: ['pantries'], }); - return assignments.map((a) => a.pantry); + if (!user) throw new NotFoundException(`User ${volunteerId} not found`); + if (!VOLUNTEER_ROLES.includes(user.role)) { + throw new BadRequestException(`User ${volunteerId} is not a volunteer`); + } + + return user.pantries; } async assignPantriesToVolunteer( volunteerId: number, pantryIds: number[], - ): Promise { + ): Promise { validateId(volunteerId, 'Volunteer'); pantryIds.forEach((id) => validateId(id, 'Pantry')); - const user = await this.repo.findOne({ where: { id: volunteerId } }); + const user = await this.repo.findOne({ + where: { id: volunteerId }, + relations: ['pantries'], + }); + if (!user) { throw new NotFoundException(`User ${volunteerId} not found`); } + if (!VOLUNTEER_ROLES.includes(user.role)) { throw new BadRequestException( - `User ${volunteerId} is not a volunteer and cannot be assigned to pantries`, + `User ${volunteerId} is not a volunteer and cannot be assigned pantries`, ); } - const pantries = await this.pantryRepo.findBy({ pantryId: In(pantryIds) }); - if (pantries.length !== pantryIds.length) { - throw new BadRequestException('One or more pantries not found'); - } - - const assignments = pantries.map((pantry) => - this.volunteerAssignmentRepo.create({ volunteer: user, pantry }), - ); + const pantries = await this.pantriesService.findByIds(pantryIds); - return this.volunteerAssignmentRepo.save(assignments); + user.pantries = [...user.pantries, ...pantries]; + return this.repo.save(user); } } diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.controller.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.controller.ts deleted file mode 100644 index 93cc638a..00000000 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.controller.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AssignmentsService } from './volunteerAssignments.service'; -import { VolunteerAssignment } from './volunteerAssignments.entity'; - -@Controller('assignments') -export class AssignmentsController { - constructor(private assignmentsService: AssignmentsService) {} - - @Get('/') - async getAssignments(): Promise { - return this.assignmentsService.getAssignments(); - } -} diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts deleted file mode 100644 index 0693b260..00000000 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -import { User } from '../users/user.entity'; -import { Pantry } from '../pantries/pantries.entity'; - -@Entity('volunteer_assignments') -export class VolunteerAssignment { - @PrimaryColumn({ name: 'volunteer_id' }) - volunteerId: number; - - @PrimaryColumn({ name: 'pantry_id' }) - pantryId: number; - - @ManyToOne(() => User, { nullable: false, onDelete: 'CASCADE' }) - @JoinColumn({ - name: 'volunteer_id', - referencedColumnName: 'id', - }) - volunteer: User; - - @ManyToOne(() => Pantry, { nullable: false, onDelete: 'CASCADE' }) - @JoinColumn({ - name: 'pantry_id', - referencedColumnName: 'pantryId', - }) - pantry: Pantry; -} diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.module.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.module.ts deleted file mode 100644 index 42e2da98..00000000 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { JwtStrategy } from '../auth/jwt.strategy'; -import { AuthService } from '../auth/auth.service'; -import { VolunteerAssignment } from './volunteerAssignments.entity'; -import { AssignmentsController } from './volunteerAssignments.controller'; -import { AssignmentsService } from './volunteerAssignments.service'; - -@Module({ - imports: [TypeOrmModule.forFeature([VolunteerAssignment])], - controllers: [AssignmentsController], - providers: [AssignmentsService, AuthService, JwtStrategy], -}) -export class AssignmentsModule {} diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts deleted file mode 100644 index e20b71f3..00000000 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { VolunteerAssignment } from './volunteerAssignments.entity'; - -@Injectable() -export class AssignmentsService { - constructor( - @InjectRepository(VolunteerAssignment) - private repo: Repository, - ) {} - - // Gets the volunteer details and the corresponding pantry - async getAssignments() { - const results = await this.repo.find({ - relations: ['volunteer', 'pantry'], - select: { - volunteer: { - id: true, - firstName: true, - lastName: true, - email: true, - phone: true, - role: true, - }, - pantry: { - pantryId: true, - pantryName: true, - }, - }, - }); - return results; - } -} From 5d83fb8c61dde68fbec54951fe87e864f28217da Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sat, 29 Nov 2025 15:33:37 -0500 Subject: [PATCH 13/18] remove volunteer assignment route and assigning duplicate pantries --- apps/backend/src/users/users.service.ts | 4 +++- apps/frontend/src/api/apiClient.ts | 5 ----- apps/frontend/src/types/types.ts | 7 ------- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 85740d50..9cdfae24 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -140,8 +140,10 @@ export class UsersService { } const pantries = await this.pantriesService.findByIds(pantryIds); + const existingPantryIds = user.pantries.map((p) => p.pantryId); + const newPantries = pantries.filter(p => !existingPantryIds.includes(p.pantryId)); - user.pantries = [...user.pantries, ...pantries]; + user.pantries = [...user.pantries, ...newPantries]; return this.repo.save(user); } } diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 74d2c812..a548bfa5 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -7,7 +7,6 @@ import { DonationItem, Donation, Allocation, - VolunteerPantryAssignment, CreateFoodRequestBody, Pantry, PantryApplicationDto, @@ -130,10 +129,6 @@ export class ApiClient { .then((response) => response.data); } - public async getAllAssignments(): Promise { - return this.get('/api/assignments') as Promise; - } - public async getVolunteers(): Promise { return this.get('/api/users/volunteers') as Promise; } diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index ce2438d3..cdea3b30 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -173,13 +173,6 @@ export enum VolunteerType { STANDARD_VOLUNTEER = 'standard_volunteer', } -export interface VolunteerPantryAssignment { - volunteerId: number; - pantryId: number; - volunteer: User; - pantry: Pantry; -} - export enum Role { ADMIN = 'admin', LEAD_VOLUNTEER = 'lead_volunteer', From 20e91e84cb355952b8967205b978a74e0919ee16 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sat, 29 Nov 2025 15:33:52 -0500 Subject: [PATCH 14/18] prettier --- apps/backend/src/users/users.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 9cdfae24..f7912a40 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -141,7 +141,9 @@ export class UsersService { const pantries = await this.pantriesService.findByIds(pantryIds); const existingPantryIds = user.pantries.map((p) => p.pantryId); - const newPantries = pantries.filter(p => !existingPantryIds.includes(p.pantryId)); + const newPantries = pantries.filter( + (p) => !existingPantryIds.includes(p.pantryId), + ); user.pantries = [...user.pantries, ...newPantries]; return this.repo.save(user); From b2a6074cdcbb5ecd87b478d8f8b522d7fac2f068 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sun, 30 Nov 2025 13:42:43 -0500 Subject: [PATCH 15/18] switch colon to comma in migration --- .../1760033134668-AddVolunteerPantryUniqueConstraint.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts index 9794796a..bc45bd3e 100644 --- a/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts +++ b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts @@ -31,7 +31,7 @@ export class AddVolunteerPantryUniqueConstraint1760033134668 ALTER TABLE volunteer_assignments ADD COLUMN assignment_id SERIAL PRIMARY KEY; ALTER TABLE volunteer_assignments - ADD CONSTRAINT fk_volunteer_id FOREIGN KEY(volunteer_id) REFERENCES users(user_id); + ADD CONSTRAINT fk_volunteer_id FOREIGN KEY(volunteer_id) REFERENCES users(user_id), ADD CONSTRAINT fk_pantry_id FOREIGN KEY(pantry_id) REFERENCES pantries(pantry_id); `); } From 62e7416c8efa97544f5836ddfad14470f724ee99 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sun, 30 Nov 2025 13:52:57 -0500 Subject: [PATCH 16/18] update user service methods and frontend types --- .../src/users/users.controller.spec.ts | 2 +- apps/backend/src/users/users.service.spec.ts | 1 + apps/backend/src/users/users.service.ts | 59 ++++++++----------- apps/frontend/src/types/types.ts | 2 + 4 files changed, 30 insertions(+), 34 deletions(-) diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 21c33026..8a22cf3a 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -90,7 +90,7 @@ describe('UsersController', () => { }); }); - describe('PUT :id/role', () => { + describe('PUT /:id/role', () => { it('should update user role with valid role', async () => { const updatedUser = { ...mockUser1, role: Role.ADMIN }; mockUserService.update.mockResolvedValue(updatedUser as User); diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 0cfcb539..db28dc0e 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -212,6 +212,7 @@ describe('UsersService', () => { expect(result).toEqual(users); expect(mockUserRepository.find).toHaveBeenCalledWith({ where: { role: In(roles) }, + relations: ['pantries'], }); }); diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index f7912a40..472689a1 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -50,6 +50,21 @@ export class UsersService { return user; } + async findVolunteer(volunteerId: number): Promise { + validateId(volunteerId, 'Volunteer'); + + const volunteer = await this.repo.findOne({ + where: { id: volunteerId }, + relations: ['pantries'], + }); + + if (!volunteer) throw new NotFoundException(`User ${volunteerId} not found`); + if (!VOLUNTEER_ROLES.includes(volunteer.role)) { + throw new BadRequestException(`User ${volunteerId} is not a volunteer`); + } + return volunteer; + } + find(email: string) { return this.repo.find({ where: { email } }); } @@ -81,16 +96,16 @@ export class UsersService { } async findUsersByRoles(roles: Role[]): Promise { - return this.repo.find({ where: { role: In(roles) } }); + return this.repo.find({ + where: { role: In(roles) }, + relations: ['pantries'], + }); } async getVolunteersAndPantryAssignments(): Promise< (Omit & { pantryIds: number[] })[] > { - const volunteers = await this.repo.find({ - where: { role: In(VOLUNTEER_ROLES) }, - relations: ['pantries'], - }); + const volunteers = await this.findUsersByRoles(VOLUNTEER_ROLES); return volunteers.map((v) => { const { pantries, ...volunteerWithoutPantries } = v; @@ -104,17 +119,8 @@ export class UsersService { async getVolunteerPantries(volunteerId: number): Promise { validateId(volunteerId, 'Volunteer'); - const user = await this.repo.findOne({ - where: { id: volunteerId }, - relations: ['pantries'], - }); - - if (!user) throw new NotFoundException(`User ${volunteerId} not found`); - if (!VOLUNTEER_ROLES.includes(user.role)) { - throw new BadRequestException(`User ${volunteerId} is not a volunteer`); - } - - return user.pantries; + const volunteer = await this.findVolunteer(volunteerId); + return volunteer.pantries; } async assignPantriesToVolunteer( @@ -124,28 +130,15 @@ export class UsersService { validateId(volunteerId, 'Volunteer'); pantryIds.forEach((id) => validateId(id, 'Pantry')); - const user = await this.repo.findOne({ - where: { id: volunteerId }, - relations: ['pantries'], - }); - - if (!user) { - throw new NotFoundException(`User ${volunteerId} not found`); - } - - if (!VOLUNTEER_ROLES.includes(user.role)) { - throw new BadRequestException( - `User ${volunteerId} is not a volunteer and cannot be assigned pantries`, - ); - } + const volunteer = await this.findVolunteer(volunteerId); const pantries = await this.pantriesService.findByIds(pantryIds); - const existingPantryIds = user.pantries.map((p) => p.pantryId); + const existingPantryIds = volunteer.pantries.map((p) => p.pantryId); const newPantries = pantries.filter( (p) => !existingPantryIds.includes(p.pantryId), ); - user.pantries = [...user.pantries, ...newPantries]; - return this.repo.save(user); + volunteer.pantries = [...volunteer.pantries, ...newPantries]; + return this.repo.save(volunteer); } } diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index cdea3b30..6fe1ad33 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -37,6 +37,7 @@ export interface Pantry { activitiesComments?: string; itemsInStock: string; needMoreOptions: string; + volunteers?: User[]; } export interface PantryApplicationDto { @@ -113,6 +114,7 @@ export interface User { lastName: string; email: string; phone: string; + pantries?: Pantry[]; } export interface FoodRequest { From 76c4a473a1c4ab23f9496103c968cfadcc9aec0e Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Sun, 30 Nov 2025 13:53:18 -0500 Subject: [PATCH 17/18] prettier --- apps/backend/src/users/users.service.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 472689a1..3e2625e0 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -58,7 +58,8 @@ export class UsersService { relations: ['pantries'], }); - if (!volunteer) throw new NotFoundException(`User ${volunteerId} not found`); + if (!volunteer) + throw new NotFoundException(`User ${volunteerId} not found`); if (!VOLUNTEER_ROLES.includes(volunteer.role)) { throw new BadRequestException(`User ${volunteerId} is not a volunteer`); } @@ -96,7 +97,7 @@ export class UsersService { } async findUsersByRoles(roles: Role[]): Promise { - return this.repo.find({ + return this.repo.find({ where: { role: In(roles) }, relations: ['pantries'], }); From 9d274e53ec2329740dac5571b3c37a22f7f96d77 Mon Sep 17 00:00:00 2001 From: amywng <147568742+amywng@users.noreply.github.com> Date: Wed, 3 Dec 2025 21:03:30 -0500 Subject: [PATCH 18/18] remove redundant validation --- apps/backend/src/users/users.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 3e2625e0..1f0984e3 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -118,8 +118,6 @@ export class UsersService { } async getVolunteerPantries(volunteerId: number): Promise { - validateId(volunteerId, 'Volunteer'); - const volunteer = await this.findVolunteer(volunteerId); return volunteer.pantries; } @@ -128,7 +126,6 @@ export class UsersService { volunteerId: number, pantryIds: number[], ): Promise { - validateId(volunteerId, 'Volunteer'); pantryIds.forEach((id) => validateId(id, 'Pantry')); const volunteer = await this.findVolunteer(volunteerId);