,
- private emailsService: EmailsService,
- ) {}
-
- // Example endpoint for email sending (DO NOT DEPLOY THIS)
- /*
- async sendTestEmail(recipient: string) {
- const imageRes = await fetch('https://placehold.co/200x200/png');
- const imageBuffer = Buffer.from(await imageRes.arrayBuffer());
-
- const dto: SendEmailDTO = {
- toEmail: recipient,
- ccEmails: ['cc@example.com'],
- bccEmails: ['bcc@example.com'],
- subject: 'Test email',
- bodyHtml: 'This is a test email sent from UsersService.
',
- attachments: [
- {
- filename: 'sample.png',
- content: imageBuffer,
- },
- ],
- };
- return this.emailsService.sendEmail(dto);
- }
- */
-
- async create(
- email: string,
- firstName: string,
- lastName: string,
- status: Status = Status.STANDARD,
- ) {
- const userId = (await this.repo.count()) + 1;
- const user = this.repo.create({
- id: userId,
- status,
- firstName,
- lastName,
- email,
- });
-
- return this.repo.save(user);
- }
-
- findOne(id: number) {
- if (!id) {
- return null;
- }
-
- return this.repo.findOneBy({ id });
- }
-
- find(email: string) {
- return this.repo.find({ where: { email } });
- }
-
- async update(id: number, attrs: Partial) {
- const user = await this.findOne(id);
-
- if (!user) {
- throw new NotFoundException('User not found');
- }
-
- Object.assign(user, attrs);
-
- return this.repo.save(user);
- }
-
- async remove(id: number) {
- const user = await this.findOne(id);
-
- if (!user) {
- throw new NotFoundException('User not found');
- }
-
- return this.repo.remove(user);
- }
-}
diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts
index 3efd63ade..d2c246fc8 100644
--- a/apps/backend/src/volunteers/volunteers.controller.ts
+++ b/apps/backend/src/volunteers/volunteers.controller.ts
@@ -1,7 +1,15 @@
import { Controller } from '@nestjs/common';
import { VolunteersService } from './volunteers.service';
+// @ApiTags('Volunteers')
+// @ApiBearerAuth()
@Controller('volunteers')
export class VolunteersController {
constructor(private volunteersService: VolunteersService) {}
+
+ // Example endpoint
+ // @Get('/:userId')
+ // async getUser(@Param('userId', ParseIntPipe) userId: number): Promise {
+ // return this.usersService.findOne(userId);
+ // }
}
diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts
index df28421df..1c6f17a61 100644
--- a/apps/backend/src/volunteers/volunteers.service.ts
+++ b/apps/backend/src/volunteers/volunteers.service.ts
@@ -9,4 +9,21 @@ export class VolunteersService {
@InjectRepository(FosterVolunteer)
private repo: Repository,
) {}
+
+ // Example service functions
+ // find(email: string) {
+ // return this.repo.find({ where: { email } });
+ // }
+
+ // async update(id: number, attrs: Partial) {
+ // const user = await this.findOne(id);
+
+ // if (!user) {
+ // throw new NotFoundException('User not found');
+ // }
+
+ // Object.assign(user, attrs);
+
+ // return this.repo.save(user);
+ // }
}
From 36f72250eb85ddf80f62eab6c69830d471b9171d Mon Sep 17 00:00:00 2001
From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com>
Date: Mon, 14 Sep 2026 00:43:32 -0400
Subject: [PATCH 08/19] Update README.md
---
README.md | 4 ----
1 file changed, 4 deletions(-)
diff --git a/README.md b/README.md
index 2653c6b12..0d8230c64 100644
--- a/README.md
+++ b/README.md
@@ -97,10 +97,6 @@ The backend can expose [Swagger UI](https://github.com/swagger-api/swagger-ui) (
**Turn it off:** Unset `SWAGGER_ENABLED` or set it to `false`.
-**TL TODOS:**
-1. In [`apps/backend/src/main.ts`](apps/backend/src/main.ts), replace `[YOUR_APP_NAME]` and other `[TL]` strings, adjust `.addTag()` entries to match your controllers,
-2. Add `@ApiBearerAuth()` onto the controller or handler for specifically protected routes. `addBearerAuth()` only states that the application supports authentication via HTTP Bearer in the Authorization header, it doesn't mark specific routes as protected. [Learn more](https://docs.nestjs.com/openapi/security#bearer-authentication)
-
**Decorators to know as you add routes!**
(import from `@nestjs/swagger`):
- `@ApiProperty()` / `@ApiPropertyOptional()`: on top of DTO fields
From 280fb3701577a62a16404ae0673b63ad1d71ec68 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Mon, 14 Sep 2026 00:47:46 -0400
Subject: [PATCH 09/19] Fixed env variables
---
apps/backend/src/config/typeorm.ts | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts
index 967c82205..fba681c15 100644
--- a/apps/backend/src/config/typeorm.ts
+++ b/apps/backend/src/config/typeorm.ts
@@ -6,11 +6,11 @@ import 'pg';
const config = {
type: 'postgres',
- host: `${process.env.DATABASE_HOST}`,
- port: parseInt(`${process.env.DATABASE_PORT}`, 10),
- database: `${process.env.DATABASE_NAME}`,
- username: `${process.env.DATABASE_USERNAME}`,
- password: `${process.env.DATABASE_PASSWORD}`,
+ host: `${process.env.NX_DB_HOST}`,
+ port: parseInt(`${process.env.NX_DB_PORT}`, 10),
+ database: `${process.env.NX_DB_DATABASE}`,
+ username: `${process.env.NX_DB_USERNAME}`,
+ password: `${process.env.NX_DB_PASSWORD}`,
autoLoadEntities: true,
synchronize: false,
namingStrategy: new PluralNamingStrategy(),
From 422462e7dff7dc35ecff699870bb5e9b99a51077 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com>
Date: Mon, 14 Sep 2026 00:48:55 -0400
Subject: [PATCH 10/19] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0d8230c64..af6b04583 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ docker run --name scaffolding-db \
**Option B — pgAdmin / existing Postgres install:**
-Create a new database (e.g. `jumpstart`) through pgAdmin or `psql`.
+Create a new database (e.g. `mspca`) through pgAdmin or `psql`.
**Configure connection strings:**
From 996ef9492db3b59dc540004eba0733cf5d606ce5 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Mon, 14 Sep 2026 00:53:13 -0400
Subject: [PATCH 11/19] Added CI jobs into README
---
README.md | 32 ++++++++++++++++++++++++++++++++
1 file changed, 32 insertions(+)
diff --git a/README.md b/README.md
index af6b04583..bec0e4aee 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,38 @@ To run both the frontend and backend with one command:
nx run-many -t serve -p frontend backend
```
+## CI (GitHub Actions)
+
+On every push and pull request, GitHub Actions runs:
+
+- Prettier — checks formatting
+- Lint - checks the linter
+- Backend tests — runs Jest test suites
+
+### Local commands
+
+Prettier:
+
+```
+yarn prettier:check # verify formatting
+yarn prettier:write # fix formatting
+```
+
+Lint:
+
+```
+yarn lint:check # runs linter
+yarn lint # fixes linting issues
+```
+
+Backend tests:
+
+```
+yarn test
+```
+
+The locally hosted frontend and backend will update live as you make changes to the code. Happy coding!
+
## Swagger
The backend can expose [Swagger UI](https://github.com/swagger-api/swagger-ui) (built from an OpenAPI document via [`@nestjs/swagger`](https://docs.nestjs.com/openapi/introduction)) so you can browse and try HTTP routes without reading controller code first.
From 2ca87253fbc39d0094ad9ab3e9f453b7f7e80895 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com>
Date: Mon, 14 Sep 2026 00:54:10 -0400
Subject: [PATCH 12/19] Fix typo in README.md for Lint formatting
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index bec0e4aee..e9e22fd4a 100644
--- a/README.md
+++ b/README.md
@@ -94,7 +94,7 @@ nx run-many -t serve -p frontend backend
On every push and pull request, GitHub Actions runs:
- Prettier — checks formatting
-- Lint - checks the linter
+- Lint — checks the linter
- Backend tests — runs Jest test suites
### Local commands
From 6ae75c272d5ef7fd0ef5657ea3b0f42acc28d7d3 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Mon, 14 Sep 2026 21:08:52 -0400
Subject: [PATCH 13/19] Added id validation util function
---
apps/backend/src/utils/validation.utils.spec.ts | 14 ++++++++++++++
apps/backend/src/utils/validation.utils.ts | 7 +++++++
2 files changed, 21 insertions(+)
create mode 100644 apps/backend/src/utils/validation.utils.spec.ts
create mode 100644 apps/backend/src/utils/validation.utils.ts
diff --git a/apps/backend/src/utils/validation.utils.spec.ts b/apps/backend/src/utils/validation.utils.spec.ts
new file mode 100644
index 000000000..6b3908403
--- /dev/null
+++ b/apps/backend/src/utils/validation.utils.spec.ts
@@ -0,0 +1,14 @@
+import { BadRequestException } from '@nestjs/common';
+import { validateId } from './validation.utils';
+
+describe('validateId', () => {
+ it('should not throw an error for a valid ID', () => {
+ expect(() => validateId(5, 'User')).not.toThrow();
+ });
+
+ it('should throw BadRequestException for ID < 1', () => {
+ expect(() => validateId(0, 'User')).toThrow(
+ new BadRequestException('Invalid User ID'),
+ );
+ });
+});
diff --git a/apps/backend/src/utils/validation.utils.ts b/apps/backend/src/utils/validation.utils.ts
new file mode 100644
index 000000000..5c4f14a78
--- /dev/null
+++ b/apps/backend/src/utils/validation.utils.ts
@@ -0,0 +1,7 @@
+import { BadRequestException } from '@nestjs/common';
+
+export function validateId(id: number, entityName: string): void {
+ if (!id || id < 1) {
+ throw new BadRequestException(`Invalid ${entityName} ID`);
+ }
+}
From a82852aaa77f34ac2f07111bb17f1db3db31f313 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Mon, 14 Sep 2026 21:29:13 -0400
Subject: [PATCH 14/19] Added volunteer coordinator relationship
---
.../src/coordinators/coordinators.entity.ts | 9 ++++++++-
.../backend/src/volunteers/volunteers.entity.ts | 17 ++++++++++++++++-
2 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/apps/backend/src/coordinators/coordinators.entity.ts b/apps/backend/src/coordinators/coordinators.entity.ts
index c0940f946..1ce7a3c06 100644
--- a/apps/backend/src/coordinators/coordinators.entity.ts
+++ b/apps/backend/src/coordinators/coordinators.entity.ts
@@ -1,5 +1,6 @@
-import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
import { Homebase } from '../types';
+import { FosterVolunteer } from '../volunteers/volunteers.entity';
@Entity('foster_coordinators')
export class FosterCoordinator {
@@ -31,4 +32,10 @@ export class FosterCoordinator {
@Column({ type: 'boolean' })
active!: boolean;
+
+ @OneToMany(
+ () => FosterVolunteer,
+ (volunteer) => volunteer.assignedCoordinator,
+ )
+ assignedVolunteers!: FosterVolunteer[];
}
diff --git a/apps/backend/src/volunteers/volunteers.entity.ts b/apps/backend/src/volunteers/volunteers.entity.ts
index c49284a9d..45633aad2 100644
--- a/apps/backend/src/volunteers/volunteers.entity.ts
+++ b/apps/backend/src/volunteers/volunteers.entity.ts
@@ -1,6 +1,13 @@
-import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
+import {
+ Entity,
+ PrimaryGeneratedColumn,
+ Column,
+ ManyToOne,
+ JoinColumn,
+} from 'typeorm';
import { Homebase } from '../types';
import { FosterType } from './volunteers.types';
+import { FosterCoordinator } from '../coordinators/coordinators.entity';
@Entity('foster_volunteers')
export class FosterVolunteer {
@@ -60,4 +67,12 @@ export class FosterVolunteer {
@Column({ type: 'boolean' })
active!: boolean;
+
+ @ManyToOne(
+ () => FosterCoordinator,
+ (coordinator) => coordinator.assignedVolunteers,
+ { nullable: true },
+ )
+ @JoinColumn({ name: 'assigned_coordinator_id' })
+ assignedCoordinator!: FosterCoordinator | null;
}
From 8048a768888c4996a3ee13d59fc783270327bd40 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com>
Date: Thu, 17 Sep 2026 23:05:27 -0400
Subject: [PATCH 15/19] Ddb/test branch (#1)
* Added comment
* Fixed pre-push
---
.husky/pre-push | 3 ---
apps/backend/src/types.ts | 1 +
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/.husky/pre-push b/.husky/pre-push
index 834ceb651..34edbdddd 100755
--- a/.husky/pre-push
+++ b/.husky/pre-push
@@ -1,6 +1,3 @@
-#!/usr/bin/env sh
-. "$(dirname -- "$0")/_/husky.sh"
-
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
diff --git a/apps/backend/src/types.ts b/apps/backend/src/types.ts
index 3c667a091..828006066 100644
--- a/apps/backend/src/types.ts
+++ b/apps/backend/src/types.ts
@@ -1,3 +1,4 @@
+// All commonly shared types go in here
export enum Homebase {
BOSTON = 'Boston',
CAPE_COD = 'Cape Cod',
From 1842757b4682b62a0128911deea0faa7eb2f5d34 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com>
Date: Sun, 20 Sep 2026 18:58:10 -0400
Subject: [PATCH 16/19] Update PostgreSQL setup instructions in README
Removed Docker setup instructions for PostgreSQL and updated password note.
---
README.md | 15 +--------------
1 file changed, 1 insertion(+), 14 deletions(-)
diff --git a/README.md b/README.md
index e9e22fd4a..67697cf69 100644
--- a/README.md
+++ b/README.md
@@ -14,19 +14,6 @@ You can optionally install `nx` globally with `npm install -g nx` - if you don't
This project uses PostgreSQL. You'll need a running Postgres instance before starting the backend.
-**Option A — Docker (recommended for local dev):**
-
-```bash
-docker run --name scaffolding-db \
- -e POSTGRES_USER=postgres \
- -e POSTGRES_PASSWORD=postgres \
- -e POSTGRES_DB=[DB NAME HERE] \
- -p 5432:5432 \
- -d postgres:15
-```
-
-**Option B — pgAdmin / existing Postgres install:**
-
Create a new database (e.g. `mspca`) through pgAdmin or `psql`.
**Configure connection strings:**
@@ -41,7 +28,7 @@ cp example.env .env
NX_DB_HOST=localhost
NX_DB_PORT=5432
NX_DB_USERNAME=postgres
-NX_DB_PASSWORD=postgres
+NX_DB_PASSWORD=postgres (replace this with your actual PGAdmin4 password)
NX_DB_DATABASE=[DB NAME HERE]
```
From 74dac85084f1cb530809bcd1fa21f4eb34cda30c Mon Sep 17 00:00:00 2001
From: Dalton Burkhart <137221425+dburkhart07@users.noreply.github.com>
Date: Mon, 21 Sep 2026 01:48:34 -0400
Subject: [PATCH 17/19] Delete .github/CODEOWNERS
---
.github/CODEOWNERS | 1 -
1 file changed, 1 deletion(-)
delete mode 100644 .github/CODEOWNERS
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
deleted file mode 100644
index ee529b859..000000000
--- a/.github/CODEOWNERS
+++ /dev/null
@@ -1 +0,0 @@
-* @maxn990
From c94b943fcd1e11520f032ec1bc3c9998a5cb72b3 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Sat, 26 Sep 2026 14:24:51 -0400
Subject: [PATCH 18/19] Added gitattributes for similar prettier formats
---
.gitattributes | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 .gitattributes
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..29471df34
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+# Normalize line endings to LF on commit, ensuring identical Prettier output across OS
+* text=auto eol=lf
From 014eb0db50a8f7a008cc44ad998509bdd67a89ee Mon Sep 17 00:00:00 2001
From: pujitakalinadhabhotla
<161961520+pujitakalinadhabhotla@users.noreply.github.com>
Date: Sun, 27 Sep 2026 17:48:36 -0400
Subject: [PATCH 19/19] MSPCA-7: Add activate/deactivate endpoints for
volunteers and coordinators
---
.../coordinators.controller.spec.ts | 45 ++++++++++-
.../coordinators/coordinators.controller.ts | 22 ++++-
.../coordinators/coordinators.service.spec.ts | 81 ++++++++++++++++++-
.../src/coordinators/coordinators.service.ts | 30 ++++++-
.../volunteers/volunteers.controller.spec.ts | 45 ++++++++++-
.../src/volunteers/volunteers.controller.ts | 22 ++++-
.../src/volunteers/volunteers.service.spec.ts | 69 +++++++++++++++-
.../src/volunteers/volunteers.service.ts | 26 +++++-
8 files changed, 332 insertions(+), 8 deletions(-)
diff --git a/apps/backend/src/coordinators/coordinators.controller.spec.ts b/apps/backend/src/coordinators/coordinators.controller.spec.ts
index 5d8f27e43..1760e99ae 100644
--- a/apps/backend/src/coordinators/coordinators.controller.spec.ts
+++ b/apps/backend/src/coordinators/coordinators.controller.spec.ts
@@ -1,17 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
+import { BadRequestException } from '@nestjs/common';
import { CoordinatorsController } from './coordinators.controller';
import { CoordinatorsService } from './coordinators.service';
describe('CoordinatorsController', () => {
let controller: CoordinatorsController;
+ let service: { deactivate: jest.Mock; activate: jest.Mock };
beforeEach(async () => {
+ service = {
+ deactivate: jest.fn(),
+ activate: jest.fn(),
+ };
+
const module: TestingModule = await Test.createTestingModule({
controllers: [CoordinatorsController],
providers: [
{
provide: CoordinatorsService,
- useValue: {},
+ useValue: service,
},
],
}).compile();
@@ -22,4 +29,40 @@ describe('CoordinatorsController', () => {
it('should be defined', () => {
expect(controller).toBeDefined();
});
+
+ describe('deactivate', () => {
+ it('should call service.deactivate with the parsed id', async () => {
+ const coordinator = { coordinatorId: 1, active: false };
+ service.deactivate.mockResolvedValue(coordinator);
+
+ const result = await controller.deactivate(1);
+
+ expect(service.deactivate).toHaveBeenCalledWith(1);
+ expect(result).toEqual(coordinator);
+ });
+
+ it('should throw BadRequestException for an invalid id', async () => {
+ await expect(controller.deactivate(0)).rejects.toThrow(
+ BadRequestException,
+ );
+ expect(service.deactivate).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('activate', () => {
+ it('should call service.activate with the parsed id', async () => {
+ const coordinator = { coordinatorId: 1, active: true };
+ service.activate.mockResolvedValue(coordinator);
+
+ const result = await controller.activate(1);
+
+ expect(service.activate).toHaveBeenCalledWith(1);
+ expect(result).toEqual(coordinator);
+ });
+
+ it('should throw BadRequestException for an invalid id', async () => {
+ await expect(controller.activate(0)).rejects.toThrow(BadRequestException);
+ expect(service.activate).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/apps/backend/src/coordinators/coordinators.controller.ts b/apps/backend/src/coordinators/coordinators.controller.ts
index bbbddfd39..1cee6c24b 100644
--- a/apps/backend/src/coordinators/coordinators.controller.ts
+++ b/apps/backend/src/coordinators/coordinators.controller.ts
@@ -1,7 +1,27 @@
-import { Controller } from '@nestjs/common';
+import { Controller, Param, ParseIntPipe, Patch } from '@nestjs/common';
import { CoordinatorsService } from './coordinators.service';
+import { validateId } from '../utils/validation.utils';
+import { FosterCoordinator } from './coordinators.entity';
@Controller('coordinators')
export class CoordinatorsController {
constructor(private coordinatorsService: CoordinatorsService) {}
+
+ @Patch('/:id/deactivate')
+ async deactivate(
+ @Param('id', ParseIntPipe) id: number,
+ ): Promise {
+ validateId(id, 'Coordinator');
+
+ return this.coordinatorsService.deactivate(id);
+ }
+
+ @Patch('/:id/activate')
+ async activate(
+ @Param('id', ParseIntPipe) id: number,
+ ): Promise {
+ validateId(id, 'Coordinator');
+
+ return this.coordinatorsService.activate(id);
+ }
}
diff --git a/apps/backend/src/coordinators/coordinators.service.spec.ts b/apps/backend/src/coordinators/coordinators.service.spec.ts
index 2003f702f..76423322e 100644
--- a/apps/backend/src/coordinators/coordinators.service.spec.ts
+++ b/apps/backend/src/coordinators/coordinators.service.spec.ts
@@ -1,18 +1,26 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
+import { NotFoundException } from '@nestjs/common';
import { CoordinatorsService } from './coordinators.service';
import { FosterCoordinator } from './coordinators.entity';
describe('CoordinatorsService', () => {
let service: CoordinatorsService;
+ let repo: { findOne: jest.Mock; save: jest.Mock; delete: jest.Mock };
beforeEach(async () => {
+ repo = {
+ findOne: jest.fn(),
+ save: jest.fn(),
+ delete: jest.fn(),
+ };
+
const module: TestingModule = await Test.createTestingModule({
providers: [
CoordinatorsService,
{
provide: getRepositoryToken(FosterCoordinator),
- useValue: {},
+ useValue: repo,
},
],
}).compile();
@@ -23,4 +31,75 @@ describe('CoordinatorsService', () => {
it('should be defined', () => {
expect(service).toBeDefined();
});
+
+ describe('deactivate', () => {
+ it('should set active to false and save the coordinator', async () => {
+ const coordinator = {
+ coordinatorId: 1,
+ active: true,
+ } as FosterCoordinator;
+ repo.findOne.mockResolvedValue(coordinator);
+ repo.save.mockResolvedValue({ ...coordinator, active: false });
+
+ const result = await service.deactivate(1);
+
+ expect(repo.findOne).toHaveBeenCalledWith({
+ where: { coordinatorId: 1 },
+ });
+ expect(repo.save).toHaveBeenCalledWith({
+ ...coordinator,
+ active: false,
+ });
+ expect(result.active).toBe(false);
+ });
+
+ it('should throw NotFoundException if coordinator does not exist', async () => {
+ repo.findOne.mockResolvedValue(null);
+
+ await expect(service.deactivate(999)).rejects.toThrow(
+ new NotFoundException('Coordinator with ID 999 not found'),
+ );
+ expect(repo.save).not.toHaveBeenCalled();
+ });
+ it('should not delete the coordinator record', async () => {
+ const coordinator = {
+ coordinatorId: 1,
+ active: true,
+ } as FosterCoordinator;
+ repo.findOne.mockResolvedValue(coordinator);
+ repo.save.mockResolvedValue({ ...coordinator, active: false });
+
+ await service.deactivate(1);
+
+ expect(repo.delete).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('activate', () => {
+ it('should set active to true and save the coordinator', async () => {
+ const coordinator = {
+ coordinatorId: 1,
+ active: false,
+ } as FosterCoordinator;
+ repo.findOne.mockResolvedValue(coordinator);
+ repo.save.mockResolvedValue({ ...coordinator, active: true });
+
+ const result = await service.activate(1);
+
+ expect(repo.findOne).toHaveBeenCalledWith({
+ where: { coordinatorId: 1 },
+ });
+ expect(repo.save).toHaveBeenCalledWith({ ...coordinator, active: true });
+ expect(result.active).toBe(true);
+ });
+
+ it('should throw NotFoundException if coordinator does not exist', async () => {
+ repo.findOne.mockResolvedValue(null);
+
+ await expect(service.activate(999)).rejects.toThrow(
+ new NotFoundException('Coordinator with ID 999 not found'),
+ );
+ expect(repo.save).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/apps/backend/src/coordinators/coordinators.service.ts b/apps/backend/src/coordinators/coordinators.service.ts
index 81fb74d9f..6b4ab0002 100644
--- a/apps/backend/src/coordinators/coordinators.service.ts
+++ b/apps/backend/src/coordinators/coordinators.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FosterCoordinator } from './coordinators.entity';
@@ -9,4 +9,32 @@ export class CoordinatorsService {
@InjectRepository(FosterCoordinator)
private repo: Repository,
) {}
+
+ async deactivate(id: number): Promise {
+ const coordinator = await this.repo.findOne({
+ where: { coordinatorId: id },
+ });
+
+ if (!coordinator) {
+ throw new NotFoundException(`Coordinator with ID ${id} not found`);
+ }
+
+ coordinator.active = false;
+
+ return this.repo.save(coordinator);
+ }
+
+ async activate(id: number): Promise {
+ const coordinator = await this.repo.findOne({
+ where: { coordinatorId: id },
+ });
+
+ if (!coordinator) {
+ throw new NotFoundException(`Coordinator with ID ${id} not found`);
+ }
+
+ coordinator.active = true;
+
+ return this.repo.save(coordinator);
+ }
}
diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts
index a3cdb3daa..0dccf3090 100644
--- a/apps/backend/src/volunteers/volunteers.controller.spec.ts
+++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts
@@ -1,17 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
+import { BadRequestException } from '@nestjs/common';
import { VolunteersController } from './volunteers.controller';
import { VolunteersService } from './volunteers.service';
describe('VolunteersController', () => {
let controller: VolunteersController;
+ let service: { deactivate: jest.Mock; activate: jest.Mock };
beforeEach(async () => {
+ service = {
+ deactivate: jest.fn(),
+ activate: jest.fn(),
+ };
+
const module: TestingModule = await Test.createTestingModule({
controllers: [VolunteersController],
providers: [
{
provide: VolunteersService,
- useValue: {},
+ useValue: service,
},
],
}).compile();
@@ -22,4 +29,40 @@ describe('VolunteersController', () => {
it('should be defined', () => {
expect(controller).toBeDefined();
});
+
+ describe('deactivate', () => {
+ it('should call service.deactivate with the parsed id', async () => {
+ const volunteer = { volunteerId: 1, active: false };
+ service.deactivate.mockResolvedValue(volunteer);
+
+ const result = await controller.deactivate(1);
+
+ expect(service.deactivate).toHaveBeenCalledWith(1);
+ expect(result).toEqual(volunteer);
+ });
+
+ it('should throw BadRequestException for an invalid id', async () => {
+ await expect(controller.deactivate(0)).rejects.toThrow(
+ BadRequestException,
+ );
+ expect(service.deactivate).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('activate', () => {
+ it('should call service.activate with the parsed id', async () => {
+ const volunteer = { volunteerId: 1, active: true };
+ service.activate.mockResolvedValue(volunteer);
+
+ const result = await controller.activate(1);
+
+ expect(service.activate).toHaveBeenCalledWith(1);
+ expect(result).toEqual(volunteer);
+ });
+
+ it('should throw BadRequestException for an invalid id', async () => {
+ await expect(controller.activate(0)).rejects.toThrow(BadRequestException);
+ expect(service.activate).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts
index d2c246fc8..cb7fbf8fa 100644
--- a/apps/backend/src/volunteers/volunteers.controller.ts
+++ b/apps/backend/src/volunteers/volunteers.controller.ts
@@ -1,5 +1,7 @@
-import { Controller } from '@nestjs/common';
+import { Controller, Param, ParseIntPipe, Patch } from '@nestjs/common';
import { VolunteersService } from './volunteers.service';
+import { validateId } from '../utils/validation.utils';
+import { FosterVolunteer } from './volunteers.entity';
// @ApiTags('Volunteers')
// @ApiBearerAuth()
@@ -12,4 +14,22 @@ export class VolunteersController {
// async getUser(@Param('userId', ParseIntPipe) userId: number): Promise {
// return this.usersService.findOne(userId);
// }
+
+ @Patch('/:id/deactivate')
+ async deactivate(
+ @Param('id', ParseIntPipe) id: number,
+ ): Promise {
+ validateId(id, 'Volunteer');
+
+ return this.volunteersService.deactivate(id);
+ }
+
+ @Patch('/:id/activate')
+ async activate(
+ @Param('id', ParseIntPipe) id: number,
+ ): Promise {
+ validateId(id, 'Volunteer');
+
+ return this.volunteersService.activate(id);
+ }
}
diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts
index fda546818..10888a681 100644
--- a/apps/backend/src/volunteers/volunteers.service.spec.ts
+++ b/apps/backend/src/volunteers/volunteers.service.spec.ts
@@ -1,18 +1,26 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
+import { NotFoundException } from '@nestjs/common';
import { VolunteersService } from './volunteers.service';
import { FosterVolunteer } from './volunteers.entity';
describe('VolunteersService', () => {
let service: VolunteersService;
+ let repo: { findOne: jest.Mock; save: jest.Mock; delete: jest.Mock };
beforeEach(async () => {
+ repo = {
+ findOne: jest.fn(),
+ save: jest.fn(),
+ delete: jest.fn(),
+ };
+
const module: TestingModule = await Test.createTestingModule({
providers: [
VolunteersService,
{
provide: getRepositoryToken(FosterVolunteer),
- useValue: {},
+ useValue: repo,
},
],
}).compile();
@@ -23,4 +31,63 @@ describe('VolunteersService', () => {
it('should be defined', () => {
expect(service).toBeDefined();
});
+
+ describe('deactivate', () => {
+ it('should set active to false and save the volunteer', async () => {
+ const volunteer = { volunteerId: 1, active: true } as FosterVolunteer;
+ repo.findOne.mockResolvedValue(volunteer);
+ repo.save.mockResolvedValue({ ...volunteer, active: false });
+
+ const result = await service.deactivate(1);
+
+ expect(repo.findOne).toHaveBeenCalledWith({
+ where: { volunteerId: 1 },
+ });
+ expect(repo.save).toHaveBeenCalledWith({ ...volunteer, active: false });
+ expect(result.active).toBe(false);
+ });
+
+ it('should throw NotFoundException if volunteer does not exist', async () => {
+ repo.findOne.mockResolvedValue(null);
+
+ await expect(service.deactivate(999)).rejects.toThrow(
+ new NotFoundException('Volunteer with ID 999 not found'),
+ );
+ expect(repo.save).not.toHaveBeenCalled();
+ });
+ it('should not delete the volunteer record', async () => {
+ const volunteer = { volunteerId: 1, active: true } as FosterVolunteer;
+ repo.findOne.mockResolvedValue(volunteer);
+ repo.save.mockResolvedValue({ ...volunteer, active: false });
+
+ await service.deactivate(1);
+
+ expect(repo.delete).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('activate', () => {
+ it('should set active to true and save the volunteer', async () => {
+ const volunteer = { volunteerId: 1, active: false } as FosterVolunteer;
+ repo.findOne.mockResolvedValue(volunteer);
+ repo.save.mockResolvedValue({ ...volunteer, active: true });
+
+ const result = await service.activate(1);
+
+ expect(repo.findOne).toHaveBeenCalledWith({
+ where: { volunteerId: 1 },
+ });
+ expect(repo.save).toHaveBeenCalledWith({ ...volunteer, active: true });
+ expect(result.active).toBe(true);
+ });
+
+ it('should throw NotFoundException if volunteer does not exist', async () => {
+ repo.findOne.mockResolvedValue(null);
+
+ await expect(service.activate(999)).rejects.toThrow(
+ new NotFoundException('Volunteer with ID 999 not found'),
+ );
+ expect(repo.save).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts
index 1c6f17a61..1a9667a68 100644
--- a/apps/backend/src/volunteers/volunteers.service.ts
+++ b/apps/backend/src/volunteers/volunteers.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FosterVolunteer } from './volunteers.entity';
@@ -26,4 +26,28 @@ export class VolunteersService {
// return this.repo.save(user);
// }
+
+ async deactivate(id: number): Promise {
+ const volunteer = await this.repo.findOne({ where: { volunteerId: id } });
+
+ if (!volunteer) {
+ throw new NotFoundException(`Volunteer with ID ${id} not found`);
+ }
+
+ volunteer.active = false;
+
+ return this.repo.save(volunteer);
+ }
+
+ async activate(id: number): Promise {
+ const volunteer = await this.repo.findOne({ where: { volunteerId: id } });
+
+ if (!volunteer) {
+ throw new NotFoundException(`Volunteer with ID ${id} not found`);
+ }
+
+ volunteer.active = true;
+
+ return this.repo.save(volunteer);
+ }
}