From 55495f71daec8386965b38d532059f853f890883 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Wed, 11 Mar 2026 01:59:08 -0400
Subject: [PATCH 1/6] Got initial implementation working
---
apps/backend/src/emails/email.service.ts | 6 +-
apps/backend/src/emails/emailTemplates.ts | 80 +++++++++++++++++++
.../manufacturers.service.ts | 29 ++++++-
.../src/foodRequests/request.service.ts | 17 ++++
.../src/pantries/pantries.controller.ts | 15 ----
apps/backend/src/pantries/pantries.service.ts | 25 ++++++
apps/backend/src/users/users.module.ts | 2 +
apps/backend/src/users/users.service.ts | 33 +++++++-
8 files changed, 185 insertions(+), 22 deletions(-)
create mode 100644 apps/backend/src/emails/emailTemplates.ts
diff --git a/apps/backend/src/emails/email.service.ts b/apps/backend/src/emails/email.service.ts
index a319e1331..9edfdaa0f 100644
--- a/apps/backend/src/emails/email.service.ts
+++ b/apps/backend/src/emails/email.service.ts
@@ -21,7 +21,7 @@ export class EmailsService {
* @param recipientEmail the email address of the recipients
* @param subject the subject of the email
* @param bodyHtml the HTML body of the email
- * @param attachments any base64 encoded attachments to inlude in the email
+ * @param attachments any base64 encoded attachments to include in the email
* @resolves if the email was sent successfully
* @rejects if the email was not sent successfully
*/
@@ -31,6 +31,10 @@ export class EmailsService {
bodyHTML: string,
attachments?: EmailAttachment[],
): Promise {
+ if (process.env.SEND_AUTOMATED_EMAILS === 'false') {
+ this.logger.warn('Automated emails are disabled. Email not sent.');
+ return Promise.resolve();
+ }
return this.amazonSESWrapper.sendEmails(
recipientEmails,
subject,
diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts
new file mode 100644
index 000000000..b572e9825
--- /dev/null
+++ b/apps/backend/src/emails/emailTemplates.ts
@@ -0,0 +1,80 @@
+export type EmailTemplate = {
+ subject: string;
+ bodyHTML: string;
+ additionalContent?: string;
+};
+
+export const emailTemplates = {
+ pantryFmApplicationApproved: (params: { name: string }): EmailTemplate => ({
+ subject: 'Your Securing Safe Food Account Has Been Approved',
+ bodyHTML: `
+ Hi ${params.name},
+
+ We're excited to let you know that your Securing Safe Food account has been
+ approved and is now active. You can now log in using the credentials created
+ during registration to begin submitting requests, managing donations, and
+ coordinating with our network.
+
+
+ If you have any questions as you get started or need help navigating the
+ platform, please do not hesitate to reach out — we are happy to help!
+
+
+ We are grateful to have you as part of the SSF community and look forward
+ to working together to expand access to allergen-safe food.
+
+ Best regards,
The Securing Safe Food Team
+ `,
+ }),
+
+ volunteerAccountCreated: (): EmailTemplate => ({
+ subject: 'Welcome to Securing Safe Food: Your Volunteer Account Is Ready',
+ bodyHTML: `
+ Welcome to Securing Safe Food!
+
+ Your volunteer account has been successfully created and you can now log in
+ to begin supporting pantry coordination, order matching, and delivery logistics.
+
+
+ Once logged in, you'll be able to view your assignments, track active requests,
+ and collaborate with partner organizations.
+
+
+ Thank you for being part of our mission. Your time and effort directly help
+ increase access to safe food for individuals with dietary restrictions.
+
+ Best regards,
The Securing Safe Food Team
+ `,
+ additionalContent: 'localhost:4200/',
+ }),
+
+ pantryFmApplicationSubmitted: (): EmailTemplate => ({
+ subject: 'New Partner Application Submitted',
+ bodyHTML: `
+ Hi,
+
+ A new partner application has been submitted through the SSF platform.
+ Please log in to the dashboard to review and take action.
+
+ Best regards,
The Securing Safe Food Team
+ `,
+ additionalContent: 'localhost:4200/',
+ }),
+
+ pantrySubmitsFoodRequest: (params: {
+ pantryName: string;
+ volunteerName: string;
+ }): EmailTemplate => ({
+ subject: `${params.pantryName} Request Requires Your Review`,
+ bodyHTML: `
+ Hi ${params.volunteerName},
+
+ A new food request has been submitted by ${params.pantryName}.
+ Please log on to the SSF platform to review these request details and begin coordination when ready.
+
+
+ Thank you for your continued support of our network and mission!.
+
Best regards,
The Securing Safe Food Team
+ `,
+ }),
+};
diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts
index ee4b49ac5..1dbb7e892 100644
--- a/apps/backend/src/foodManufacturers/manufacturers.service.ts
+++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts
@@ -10,6 +10,8 @@ import { ApplicationStatus } from '../shared/types';
import { userSchemaDto } from '../users/dtos/userSchema.dto';
import { UsersService } from '../users/users.service';
import { Donation } from '../donations/donations.entity';
+import { emailTemplates } from '../emails/emailTemplates';
+import { EmailsService } from '../emails/email.service';
@Injectable()
export class FoodManufacturersService {
@@ -18,6 +20,7 @@ export class FoodManufacturersService {
private repo: Repository,
private usersService: UsersService,
+ private emailsService: EmailsService,
@InjectRepository(Donation)
private donationsRepo: Repository,
@@ -114,6 +117,15 @@ export class FoodManufacturersService {
foodManufacturerData.newsletterSubscription ?? null;
await this.repo.save(foodManufacturer);
+
+ // TODO: Change receiver if deemed that they shouldn't receive an email on submisssion
+ if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ await this.emailsService.sendEmails(
+ [foodManufacturer.foodManufacturerRepresentative.email],
+ emailTemplates.pantryFmApplicationSubmitted().subject,
+ emailTemplates.pantryFmApplicationSubmitted().bodyHTML,
+ );
+ }
}
async approve(id: number) {
@@ -127,10 +139,7 @@ export class FoodManufacturersService {
}
const createUserDto: userSchemaDto = {
- email: foodManufacturer.foodManufacturerRepresentative.email,
- firstName: foodManufacturer.foodManufacturerRepresentative.firstName,
- lastName: foodManufacturer.foodManufacturerRepresentative.lastName,
- phone: foodManufacturer.foodManufacturerRepresentative.phone,
+ ...foodManufacturer.foodManufacturerRepresentative,
role: Role.FOODMANUFACTURER,
};
@@ -140,6 +149,18 @@ export class FoodManufacturersService {
status: ApplicationStatus.APPROVED,
foodManufacturerRepresentative: newFoodManufacturer,
});
+
+ if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ await this.emailsService.sendEmails(
+ [newFoodManufacturer.email],
+ emailTemplates.pantryFmApplicationApproved({
+ name: newFoodManufacturer.firstName,
+ }).subject,
+ emailTemplates.pantryFmApplicationApproved({
+ name: newFoodManufacturer.firstName,
+ }).bodyHTML,
+ );
+ }
}
async deny(id: number) {
diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts
index 0a3290fd2..8d76e1255 100644
--- a/apps/backend/src/foodRequests/request.service.ts
+++ b/apps/backend/src/foodRequests/request.service.ts
@@ -15,6 +15,8 @@ import {
} from './dtos/matching.dto';
import { FoodType } from '../donationItems/types';
import { DonationItem } from '../donationItems/donationItems.entity';
+import { EmailsService } from '../emails/email.service';
+import { emailTemplates } from '../emails/emailTemplates';
@Injectable()
export class RequestsService {
@@ -26,6 +28,7 @@ export class RequestsService {
private foodManufacturerRepo: Repository,
@InjectRepository(DonationItem)
private donationItemRepo: Repository,
+ private emailsService: EmailsService,
) {}
async findOne(requestId: number): Promise {
@@ -210,6 +213,20 @@ export class RequestsService {
additionalInformation,
});
+ if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ await this.emailsService.sendEmails(
+ [foodRequest.pantry.pantryUser.email],
+ emailTemplates.pantrySubmitsFoodRequest({
+ pantryName: foodRequest.pantry.pantryName,
+ volunteerName: foodRequest.pantry.pantryUser.firstName,
+ }).subject,
+ emailTemplates.pantrySubmitsFoodRequest({
+ pantryName: foodRequest.pantry.pantryName,
+ volunteerName: foodRequest.pantry.pantryUser.firstName,
+ }).bodyHTML,
+ );
+ }
+
return await this.repo.save(foodRequest);
}
diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts
index d5e252a05..031f5dd7f 100644
--- a/apps/backend/src/pantries/pantries.controller.ts
+++ b/apps/backend/src/pantries/pantries.controller.ts
@@ -26,8 +26,6 @@ import {
import { Order } from '../orders/order.entity';
import { OrdersService } from '../orders/order.service';
import { CheckOwnership, pipeNullable } from '../auth/ownership.decorator';
-import { EmailsService } from '../emails/email.service';
-import { SendEmailDTO } from '../emails/dto/send-email.dto';
import { Public } from '../auth/public.decorator';
import { AuthenticatedRequest } from '../auth/authenticated-request';
@@ -36,7 +34,6 @@ export class PantriesController {
constructor(
private pantriesService: PantriesService,
private ordersService: OrdersService,
- private emailsService: EmailsService,
) {}
@Roles(Role.PANTRY)
@@ -340,16 +337,4 @@ export class PantriesController {
): Promise {
return this.pantriesService.deny(pantryId);
}
-
- @Post('/email')
- async sendEmail(@Body() sendEmailDTO: SendEmailDTO): Promise {
- const { toEmails, subject, bodyHtml, attachments } = sendEmailDTO;
-
- await this.emailsService.sendEmails(
- toEmails,
- subject,
- bodyHtml,
- attachments,
- );
- }
}
diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts
index d21cd516e..289d4bfcf 100644
--- a/apps/backend/src/pantries/pantries.service.ts
+++ b/apps/backend/src/pantries/pantries.service.ts
@@ -14,6 +14,8 @@ import { PantryApplicationDto } from './dtos/pantry-application.dto';
import { Role } from '../users/types';
import { userSchemaDto } from '../users/dtos/userSchema.dto';
import { UsersService } from '../users/users.service';
+import { emailTemplates } from '../emails/emailTemplates';
+import { EmailsService } from '../emails/email.service';
@Injectable()
export class PantriesService {
@@ -22,6 +24,9 @@ export class PantriesService {
@Inject(forwardRef(() => UsersService))
private usersService: UsersService,
+
+ @Inject(forwardRef(() => EmailsService))
+ private emailsService: EmailsService,
) {}
async findOne(pantryId: number): Promise {
@@ -104,6 +109,14 @@ export class PantriesService {
// pantry contact is automatically added to User table
await this.repo.save(pantry);
+
+ if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ await this.emailsService.sendEmails(
+ [pantryContact.email],
+ emailTemplates.pantryFmApplicationSubmitted().subject,
+ emailTemplates.pantryFmApplicationSubmitted().bodyHTML,
+ );
+ }
}
async approve(id: number) {
@@ -128,6 +141,18 @@ export class PantriesService {
status: ApplicationStatus.APPROVED,
pantryUser: newPantryUser,
});
+
+ if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ await this.emailsService.sendEmails(
+ [newPantryUser.email],
+ emailTemplates.pantryFmApplicationApproved({
+ name: newPantryUser.firstName,
+ }).subject,
+ emailTemplates.pantryFmApplicationApproved({
+ name: newPantryUser.firstName,
+ }).bodyHTML,
+ );
+ }
}
async deny(id: number) {
diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts
index f7bf1c194..7408b6c1f 100644
--- a/apps/backend/src/users/users.module.ts
+++ b/apps/backend/src/users/users.module.ts
@@ -5,12 +5,14 @@ import { UsersService } from './users.service';
import { User } from './users.entity';
import { PantriesModule } from '../pantries/pantries.module';
import { AuthModule } from '../auth/auth.module';
+import { EmailsModule } from '../emails/email.module';
@Module({
imports: [
TypeOrmModule.forFeature([User]),
forwardRef(() => PantriesModule),
forwardRef(() => AuthModule),
+ EmailsModule,
],
controllers: [UsersController],
providers: [UsersService],
diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts
index 0377a6168..b23b26da5 100644
--- a/apps/backend/src/users/users.service.ts
+++ b/apps/backend/src/users/users.service.ts
@@ -1,4 +1,4 @@
-import { Injectable, NotFoundException } from '@nestjs/common';
+import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { User } from './users.entity';
@@ -6,6 +6,8 @@ import { Role } from './types';
import { validateId } from '../utils/validation.utils';
import { AuthService } from '../auth/auth.service';
import { userSchemaDto } from './dtos/userSchema.dto';
+import { emailTemplates } from '../emails/emailTemplates';
+import { EmailsService } from '../emails/email.service';
@Injectable()
export class UsersService {
@@ -14,11 +16,29 @@ export class UsersService {
private repo: Repository,
private authService: AuthService,
+
+ @Inject(EmailsService)
+ private emailsService: EmailsService,
) {}
async create(createUserDto: userSchemaDto): Promise {
const { email, firstName, lastName, phone, role } = createUserDto;
+ const emailsEnabled = process.env.SEND_AUTOMATED_EMAILS === 'true';
+
+ // Just save to DB if emails are disabled
+ if (!emailsEnabled) {
+ const user = this.repo.create({
+ role,
+ firstName,
+ lastName,
+ email,
+ phone,
+ });
+ return this.repo.save(user);
+ }
+ // Pantry and food manufacturer users must already exist in the DB
+ // (created during application) before a Cognito account is made
if (role === Role.PANTRY || role === Role.FOODMANUFACTURER) {
const existingUser = await this.repo.findOneBy({ email });
if (!existingUser) {
@@ -32,6 +52,7 @@ export class UsersService {
return this.repo.save(existingUser);
}
+ // All other roles (e.g. VOLUNTEER): create Cognito user and save to DB
const userCognitoSub = await this.authService.adminCreateUser({
firstName,
lastName,
@@ -45,7 +66,15 @@ export class UsersService {
phone,
userCognitoSub,
});
- return this.repo.save(user);
+ await this.repo.save(user);
+
+ // Send welcome email to new volunteers
+ if (role === Role.VOLUNTEER) {
+ const { subject, bodyHTML } = emailTemplates.volunteerAccountCreated();
+ await this.emailsService.sendEmails([email], subject, bodyHTML);
+ }
+
+ return user;
}
async findOne(id: number): Promise {
From e97b10fc704bb6c0bca33b00c016d01202d2a470 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Thu, 12 Mar 2026 23:40:44 -0400
Subject: [PATCH 2/6] Final commit before writing tests
---
apps/backend/src/emails/emailTemplates.ts | 8 ++++--
.../foodManufacturers/manufacturers.module.ts | 2 ++
.../manufacturers.service.ts | 22 ++++++++--------
.../src/foodRequests/request.module.ts | 2 ++
.../src/foodRequests/request.service.spec.ts | 8 ++++++
.../src/foodRequests/request.service.ts | 25 +++++++++++--------
apps/backend/src/pantries/pantries.service.ts | 21 ++++++++--------
apps/backend/src/users/users.service.spec.ts | 8 ++++++
apps/backend/src/users/users.service.ts | 10 +++++---
.../src/volunteers/volunteers.service.spec.ts | 8 ++++++
10 files changed, 78 insertions(+), 36 deletions(-)
diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts
index b572e9825..c3ea19967 100644
--- a/apps/backend/src/emails/emailTemplates.ts
+++ b/apps/backend/src/emails/emailTemplates.ts
@@ -4,6 +4,10 @@ export type EmailTemplate = {
additionalContent?: string;
};
+export const EMAIL_REDIRECT_URL = 'localhost:4200';
+// TODO: Change this before production to be the actual ssf email
+export const SSF_PARTNER_EMAIL = 'example@gmail.com';
+
export const emailTemplates = {
pantryFmApplicationApproved: (params: { name: string }): EmailTemplate => ({
subject: 'Your Securing Safe Food Account Has Been Approved',
@@ -45,7 +49,7 @@ export const emailTemplates = {
Best regards,
The Securing Safe Food Team
`,
- additionalContent: 'localhost:4200/',
+ additionalContent: EMAIL_REDIRECT_URL + '/login',
}),
pantryFmApplicationSubmitted: (): EmailTemplate => ({
@@ -58,7 +62,7 @@ export const emailTemplates = {
Best regards,
The Securing Safe Food Team
`,
- additionalContent: 'localhost:4200/',
+ additionalContent: EMAIL_REDIRECT_URL + '/approve-pantries',
}),
pantrySubmitsFoodRequest: (params: {
diff --git a/apps/backend/src/foodManufacturers/manufacturers.module.ts b/apps/backend/src/foodManufacturers/manufacturers.module.ts
index e80b08f5e..8fe5e5c6c 100644
--- a/apps/backend/src/foodManufacturers/manufacturers.module.ts
+++ b/apps/backend/src/foodManufacturers/manufacturers.module.ts
@@ -5,11 +5,13 @@ import { FoodManufacturersController } from './manufacturers.controller';
import { FoodManufacturersService } from './manufacturers.service';
import { UsersModule } from '../users/users.module';
import { Donation } from '../donations/donations.entity';
+import { EmailsModule } from '../emails/email.module';
@Module({
imports: [
TypeOrmModule.forFeature([FoodManufacturer, Donation]),
forwardRef(() => UsersModule),
+ EmailsModule,
],
controllers: [FoodManufacturersController],
providers: [FoodManufacturersService],
diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts
index 1dbb7e892..58193a523 100644
--- a/apps/backend/src/foodManufacturers/manufacturers.service.ts
+++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts
@@ -10,7 +10,7 @@ import { ApplicationStatus } from '../shared/types';
import { userSchemaDto } from '../users/dtos/userSchema.dto';
import { UsersService } from '../users/users.service';
import { Donation } from '../donations/donations.entity';
-import { emailTemplates } from '../emails/emailTemplates';
+import { emailTemplates, SSF_PARTNER_EMAIL } from '../emails/emailTemplates';
import { EmailsService } from '../emails/email.service';
@Injectable()
@@ -120,10 +120,11 @@ export class FoodManufacturersService {
// TODO: Change receiver if deemed that they shouldn't receive an email on submisssion
if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ const message = emailTemplates.pantryFmApplicationSubmitted();
await this.emailsService.sendEmails(
- [foodManufacturer.foodManufacturerRepresentative.email],
- emailTemplates.pantryFmApplicationSubmitted().subject,
- emailTemplates.pantryFmApplicationSubmitted().bodyHTML,
+ [SSF_PARTNER_EMAIL],
+ message.subject,
+ message.bodyHTML,
);
}
}
@@ -133,6 +134,7 @@ export class FoodManufacturersService {
const foodManufacturer = await this.repo.findOne({
where: { foodManufacturerId: id },
+ relations: ['foodManufacturerRepresentative'],
});
if (!foodManufacturer) {
throw new NotFoundException(`Food Manufacturer ${id} not found`);
@@ -151,14 +153,14 @@ export class FoodManufacturersService {
});
if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ const message = emailTemplates.pantryFmApplicationApproved({
+ name: newFoodManufacturer.firstName,
+ });
+
await this.emailsService.sendEmails(
[newFoodManufacturer.email],
- emailTemplates.pantryFmApplicationApproved({
- name: newFoodManufacturer.firstName,
- }).subject,
- emailTemplates.pantryFmApplicationApproved({
- name: newFoodManufacturer.firstName,
- }).bodyHTML,
+ message.subject,
+ message.bodyHTML,
);
}
}
diff --git a/apps/backend/src/foodRequests/request.module.ts b/apps/backend/src/foodRequests/request.module.ts
index d1cadf066..dcec30601 100644
--- a/apps/backend/src/foodRequests/request.module.ts
+++ b/apps/backend/src/foodRequests/request.module.ts
@@ -8,6 +8,7 @@ import { Order } from '../orders/order.entity';
import { Pantry } from '../pantries/pantries.entity';
import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';
import { DonationItem } from '../donationItems/donationItems.entity';
+import { EmailsModule } from '../emails/email.module';
@Module({
imports: [
@@ -19,6 +20,7 @@ import { DonationItem } from '../donationItems/donationItems.entity';
DonationItem,
]),
AuthModule,
+ EmailsModule,
],
controllers: [RequestsController],
providers: [RequestsService],
diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts
index d404d62a0..c57fb622d 100644
--- a/apps/backend/src/foodRequests/request.service.spec.ts
+++ b/apps/backend/src/foodRequests/request.service.spec.ts
@@ -11,6 +11,7 @@ import { FoodType } from '../donationItems/types';
import { DonationItem } from '../donationItems/donationItems.entity';
import { testDataSource } from '../config/typeormTestDataSource';
import { NotFoundException } from '@nestjs/common';
+import { EmailsService } from '../emails/email.service';
jest.setTimeout(60000);
@@ -25,6 +26,7 @@ describe('RequestsService', () => {
const module = await Test.createTestingModule({
providers: [
RequestsService,
+ EmailsService,
{
provide: getRepositoryToken(FoodRequest),
useValue: testDataSource.getRepository(FoodRequest),
@@ -45,6 +47,12 @@ describe('RequestsService', () => {
provide: getRepositoryToken(DonationItem),
useValue: testDataSource.getRepository(DonationItem),
},
+ {
+ provide: EmailsService,
+ useValue: {
+ sendEmails: jest.fn().mockResolvedValue(undefined),
+ },
+ },
],
}).compile();
diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts
index 8d76e1255..728d827a0 100644
--- a/apps/backend/src/foodRequests/request.service.ts
+++ b/apps/backend/src/foodRequests/request.service.ts
@@ -201,7 +201,10 @@ export class RequestsService {
): Promise {
validateId(pantryId, 'Pantry');
- const pantry = await this.pantryRepo.findOneBy({ pantryId });
+ const pantry = await this.pantryRepo.findOne({
+ where: { pantryId },
+ relations: ['pantryUser', 'volunteers'],
+ });
if (!pantry) {
throw new NotFoundException(`Pantry ${pantryId} not found`);
}
@@ -214,16 +217,18 @@ export class RequestsService {
});
if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ const volunteers = pantry.volunteers || [];
+ const volunteerEmails = volunteers.map((v) => v.email);
+
+ const message = emailTemplates.pantrySubmitsFoodRequest({
+ pantryName: pantry.pantryName,
+ volunteerName: pantry.pantryUser.firstName,
+ });
+
await this.emailsService.sendEmails(
- [foodRequest.pantry.pantryUser.email],
- emailTemplates.pantrySubmitsFoodRequest({
- pantryName: foodRequest.pantry.pantryName,
- volunteerName: foodRequest.pantry.pantryUser.firstName,
- }).subject,
- emailTemplates.pantrySubmitsFoodRequest({
- pantryName: foodRequest.pantry.pantryName,
- volunteerName: foodRequest.pantry.pantryUser.firstName,
- }).bodyHTML,
+ volunteerEmails,
+ message.subject,
+ message.bodyHTML,
);
}
diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts
index 289d4bfcf..552407f70 100644
--- a/apps/backend/src/pantries/pantries.service.ts
+++ b/apps/backend/src/pantries/pantries.service.ts
@@ -14,7 +14,7 @@ import { PantryApplicationDto } from './dtos/pantry-application.dto';
import { Role } from '../users/types';
import { userSchemaDto } from '../users/dtos/userSchema.dto';
import { UsersService } from '../users/users.service';
-import { emailTemplates } from '../emails/emailTemplates';
+import { emailTemplates, SSF_PARTNER_EMAIL } from '../emails/emailTemplates';
import { EmailsService } from '../emails/email.service';
@Injectable()
@@ -111,10 +111,11 @@ export class PantriesService {
await this.repo.save(pantry);
if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ const message = emailTemplates.pantryFmApplicationSubmitted();
await this.emailsService.sendEmails(
- [pantryContact.email],
- emailTemplates.pantryFmApplicationSubmitted().subject,
- emailTemplates.pantryFmApplicationSubmitted().bodyHTML,
+ [SSF_PARTNER_EMAIL],
+ message.subject,
+ message.bodyHTML,
);
}
}
@@ -143,14 +144,14 @@ export class PantriesService {
});
if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
+ const message = emailTemplates.pantryFmApplicationApproved({
+ name: newPantryUser.firstName,
+ });
+
await this.emailsService.sendEmails(
[newPantryUser.email],
- emailTemplates.pantryFmApplicationApproved({
- name: newPantryUser.firstName,
- }).subject,
- emailTemplates.pantryFmApplicationApproved({
- name: newPantryUser.firstName,
- }).bodyHTML,
+ message.subject,
+ message.bodyHTML,
);
}
}
diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts
index 4207622ef..a488bd573 100644
--- a/apps/backend/src/users/users.service.spec.ts
+++ b/apps/backend/src/users/users.service.spec.ts
@@ -11,6 +11,7 @@ import { BadRequestException } from '@nestjs/common';
import { PantriesService } from '../pantries/pantries.service';
import { userSchemaDto } from './dtos/userSchema.dto';
import { AuthService } from '../auth/auth.service';
+import { EmailsService } from '../emails/email.service';
const mockUserRepository = mock>();
const mockPantriesService = mock();
@@ -40,6 +41,7 @@ describe('UsersService', () => {
const module = await Test.createTestingModule({
providers: [
UsersService,
+ EmailsService,
{
provide: getRepositoryToken(User),
useValue: mockUserRepository,
@@ -52,6 +54,12 @@ describe('UsersService', () => {
provide: AuthService,
useValue: mockAuthService,
},
+ {
+ provide: EmailsService,
+ useValue: {
+ sendEmails: jest.fn().mockResolvedValue(undefined),
+ },
+ },
],
}).compile();
diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts
index b23b26da5..1e76f054e 100644
--- a/apps/backend/src/users/users.service.ts
+++ b/apps/backend/src/users/users.service.ts
@@ -16,8 +16,6 @@ export class UsersService {
private repo: Repository,
private authService: AuthService,
-
- @Inject(EmailsService)
private emailsService: EmailsService,
) {}
@@ -70,8 +68,12 @@ export class UsersService {
// Send welcome email to new volunteers
if (role === Role.VOLUNTEER) {
- const { subject, bodyHTML } = emailTemplates.volunteerAccountCreated();
- await this.emailsService.sendEmails([email], subject, bodyHTML);
+ const message = emailTemplates.volunteerAccountCreated();
+ await this.emailsService.sendEmails(
+ [email],
+ message.subject,
+ message.bodyHTML,
+ );
}
return user;
diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts
index 1bbcc7530..83c40505d 100644
--- a/apps/backend/src/volunteers/volunteers.service.spec.ts
+++ b/apps/backend/src/volunteers/volunteers.service.spec.ts
@@ -8,6 +8,7 @@ import { testDataSource } from '../config/typeormTestDataSource';
import { UsersService } from '../users/users.service';
import { PantriesService } from '../pantries/pantries.service';
import { AuthService } from '../auth/auth.service';
+import { EmailsService } from '../emails/email.service';
jest.setTimeout(60000);
@@ -25,6 +26,7 @@ describe('VolunteersService', () => {
VolunteersService,
UsersService,
PantriesService,
+ EmailsService,
{
provide: AuthService,
useValue: {},
@@ -37,6 +39,12 @@ describe('VolunteersService', () => {
provide: getRepositoryToken(Pantry),
useValue: testDataSource.getRepository(Pantry),
},
+ {
+ provide: EmailsService,
+ useValue: {
+ sendEmails: jest.fn().mockResolvedValue(undefined),
+ },
+ },
],
}).compile();
From 9b6d63eda0ba0c6fa3affa3cd5935b3e5c0d3369 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Fri, 13 Mar 2026 00:06:03 -0400
Subject: [PATCH 3/6] Cleaned up check for usage of send email flag
---
apps/backend/src/emails/awsSes.wrapper.ts | 3 ++
.../manufacturers.service.ts | 32 ++++++++-----------
.../src/foodRequests/request.service.ts | 28 ++++++++--------
apps/backend/src/pantries/pantries.service.ts | 32 ++++++++-----------
apps/backend/src/users/users.service.ts | 2 +-
5 files changed, 45 insertions(+), 52 deletions(-)
diff --git a/apps/backend/src/emails/awsSes.wrapper.ts b/apps/backend/src/emails/awsSes.wrapper.ts
index fd0750d58..4d2849bcb 100644
--- a/apps/backend/src/emails/awsSes.wrapper.ts
+++ b/apps/backend/src/emails/awsSes.wrapper.ts
@@ -57,6 +57,9 @@ export class AmazonSESWrapper {
const messageData = await new MailComposer(mailOptions).compile().build();
const command = new SendEmailCommand({
+ Destination: {
+ ToAddresses: recipientEmails,
+ },
Content: {
Raw: {
Data: messageData,
diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts
index 58193a523..4e72399c6 100644
--- a/apps/backend/src/foodManufacturers/manufacturers.service.ts
+++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts
@@ -119,14 +119,12 @@ export class FoodManufacturersService {
await this.repo.save(foodManufacturer);
// TODO: Change receiver if deemed that they shouldn't receive an email on submisssion
- if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
- const message = emailTemplates.pantryFmApplicationSubmitted();
- await this.emailsService.sendEmails(
- [SSF_PARTNER_EMAIL],
- message.subject,
- message.bodyHTML,
- );
- }
+ const message = emailTemplates.pantryFmApplicationSubmitted();
+ await this.emailsService.sendEmails(
+ [SSF_PARTNER_EMAIL],
+ message.subject,
+ message.bodyHTML,
+ );
}
async approve(id: number) {
@@ -152,17 +150,15 @@ export class FoodManufacturersService {
foodManufacturerRepresentative: newFoodManufacturer,
});
- if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
- const message = emailTemplates.pantryFmApplicationApproved({
- name: newFoodManufacturer.firstName,
- });
+ const message = emailTemplates.pantryFmApplicationApproved({
+ name: newFoodManufacturer.firstName,
+ });
- await this.emailsService.sendEmails(
- [newFoodManufacturer.email],
- message.subject,
- message.bodyHTML,
- );
- }
+ await this.emailsService.sendEmails(
+ [newFoodManufacturer.email],
+ message.subject,
+ message.bodyHTML,
+ );
}
async deny(id: number) {
diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts
index 728d827a0..dc6b71162 100644
--- a/apps/backend/src/foodRequests/request.service.ts
+++ b/apps/backend/src/foodRequests/request.service.ts
@@ -216,21 +216,19 @@ export class RequestsService {
additionalInformation,
});
- if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
- const volunteers = pantry.volunteers || [];
- const volunteerEmails = volunteers.map((v) => v.email);
-
- const message = emailTemplates.pantrySubmitsFoodRequest({
- pantryName: pantry.pantryName,
- volunteerName: pantry.pantryUser.firstName,
- });
-
- await this.emailsService.sendEmails(
- volunteerEmails,
- message.subject,
- message.bodyHTML,
- );
- }
+ const volunteers = pantry.volunteers || [];
+ const volunteerEmails = volunteers.map((v) => v.email);
+
+ const message = emailTemplates.pantrySubmitsFoodRequest({
+ pantryName: pantry.pantryName,
+ volunteerName: pantry.pantryUser.firstName,
+ });
+
+ await this.emailsService.sendEmails(
+ volunteerEmails,
+ message.subject,
+ message.bodyHTML,
+ );
return await this.repo.save(foodRequest);
}
diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts
index 552407f70..716119557 100644
--- a/apps/backend/src/pantries/pantries.service.ts
+++ b/apps/backend/src/pantries/pantries.service.ts
@@ -110,14 +110,12 @@ export class PantriesService {
// pantry contact is automatically added to User table
await this.repo.save(pantry);
- if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
- const message = emailTemplates.pantryFmApplicationSubmitted();
- await this.emailsService.sendEmails(
- [SSF_PARTNER_EMAIL],
- message.subject,
- message.bodyHTML,
- );
- }
+ const message = emailTemplates.pantryFmApplicationSubmitted();
+ await this.emailsService.sendEmails(
+ [SSF_PARTNER_EMAIL],
+ message.subject,
+ message.bodyHTML,
+ );
}
async approve(id: number) {
@@ -143,17 +141,15 @@ export class PantriesService {
pantryUser: newPantryUser,
});
- if (process.env.SEND_AUTOMATED_EMAILS === 'true') {
- const message = emailTemplates.pantryFmApplicationApproved({
- name: newPantryUser.firstName,
- });
+ const message = emailTemplates.pantryFmApplicationApproved({
+ name: newPantryUser.firstName,
+ });
- await this.emailsService.sendEmails(
- [newPantryUser.email],
- message.subject,
- message.bodyHTML,
- );
- }
+ await this.emailsService.sendEmails(
+ [newPantryUser.email],
+ message.subject,
+ message.bodyHTML,
+ );
}
async deny(id: number) {
diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts
index 1e76f054e..0a6a5c549 100644
--- a/apps/backend/src/users/users.service.ts
+++ b/apps/backend/src/users/users.service.ts
@@ -23,7 +23,7 @@ export class UsersService {
const { email, firstName, lastName, phone, role } = createUserDto;
const emailsEnabled = process.env.SEND_AUTOMATED_EMAILS === 'true';
- // Just save to DB if emails are disabled
+ // Just save to DB if emails are disabled (no Cognito creation)
if (!emailsEnabled) {
const user = this.repo.create({
role,
From 578e4a549aaf8dee935dd35cf406b0053956e874 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Fri, 13 Mar 2026 18:28:44 -0400
Subject: [PATCH 4/6] Added confirmed application submission for FMs and
pantries
---
apps/backend/src/emails/emailTemplates.ts | 24 ++++++++++++++++---
.../manufacturers.service.ts | 18 ++++++++++----
apps/backend/src/pantries/pantries.service.ts | 16 ++++++++++---
3 files changed, 48 insertions(+), 10 deletions(-)
diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts
index c3ea19967..ffbdc5996 100644
--- a/apps/backend/src/emails/emailTemplates.ts
+++ b/apps/backend/src/emails/emailTemplates.ts
@@ -48,11 +48,13 @@ export const emailTemplates = {
increase access to safe food for individuals with dietary restrictions.
Best regards,
The Securing Safe Food Team
+
+ To log in to your account, please click the following link: ${EMAIL_REDIRECT_URL}/login
+
`,
- additionalContent: EMAIL_REDIRECT_URL + '/login',
}),
- pantryFmApplicationSubmitted: (): EmailTemplate => ({
+ pantryFmApplicationSubmittedToAdmin: (): EmailTemplate => ({
subject: 'New Partner Application Submitted',
bodyHTML: `
Hi,
@@ -61,8 +63,24 @@ export const emailTemplates = {
Please log in to the dashboard to review and take action.
Best regards,
The Securing Safe Food Team
+
+ To review this application, please enter the admin pantry approval dashboard: ${EMAIL_REDIRECT_URL}/approve-pantries
+
+ `,
+ }),
+
+ pantryFmApplicationSubmittedToUser: (params: {
+ name: string;
+ }): EmailTemplate => ({
+ subject: 'Your Application Has Been Submitted',
+ bodyHTML: `
+ Hi ${params.name},
+
+ Thank you for your interest in partnering with Securing Safe Food!
+ Your application has been successfully submitted and is currently under review. We will notify you via email once a decision has been made.
+
+ Best regards,
The Securing Safe Food Team
`,
- additionalContent: EMAIL_REDIRECT_URL + '/approve-pantries',
}),
pantrySubmitsFoodRequest: (params: {
diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts
index 4e72399c6..f5b78268b 100644
--- a/apps/backend/src/foodManufacturers/manufacturers.service.ts
+++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts
@@ -118,12 +118,22 @@ export class FoodManufacturersService {
await this.repo.save(foodManufacturer);
- // TODO: Change receiver if deemed that they shouldn't receive an email on submisssion
- const message = emailTemplates.pantryFmApplicationSubmitted();
+ const manufacturerMessage =
+ emailTemplates.pantryFmApplicationSubmittedToUser({
+ name: foodManufacturerContact.firstName,
+ });
+
+ await this.emailsService.sendEmails(
+ [foodManufacturerContact.email],
+ manufacturerMessage.subject,
+ manufacturerMessage.bodyHTML,
+ );
+
+ const adminMessage = emailTemplates.pantryFmApplicationSubmittedToAdmin();
await this.emailsService.sendEmails(
[SSF_PARTNER_EMAIL],
- message.subject,
- message.bodyHTML,
+ adminMessage.subject,
+ adminMessage.bodyHTML,
);
}
diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts
index 716119557..258ad5883 100644
--- a/apps/backend/src/pantries/pantries.service.ts
+++ b/apps/backend/src/pantries/pantries.service.ts
@@ -110,11 +110,21 @@ export class PantriesService {
// pantry contact is automatically added to User table
await this.repo.save(pantry);
- const message = emailTemplates.pantryFmApplicationSubmitted();
+ const pantryMessage = emailTemplates.pantryFmApplicationSubmittedToUser({
+ name: pantryContact.firstName,
+ });
+
+ await this.emailsService.sendEmails(
+ [pantryContact.email],
+ pantryMessage.subject,
+ pantryMessage.bodyHTML,
+ );
+
+ const adminMessage = emailTemplates.pantryFmApplicationSubmittedToAdmin();
await this.emailsService.sendEmails(
[SSF_PARTNER_EMAIL],
- message.subject,
- message.bodyHTML,
+ adminMessage.subject,
+ adminMessage.bodyHTML,
);
}
From 4ad7344966aed39b94ec2d21a7f9804698b0cc15 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Sat, 14 Mar 2026 23:47:00 -0400
Subject: [PATCH 5/6] Final commit
---
.../manufaacturers.service.spec.ts | 292 ++++++++++++++++++
.../src/foodRequests/request.service.spec.ts | 37 ++-
.../src/pantries/pantries.service.spec.ts | 128 ++++----
apps/backend/src/users/users.service.spec.ts | 34 +-
4 files changed, 431 insertions(+), 60 deletions(-)
create mode 100644 apps/backend/src/foodManufacturers/manufaacturers.service.spec.ts
diff --git a/apps/backend/src/foodManufacturers/manufaacturers.service.spec.ts b/apps/backend/src/foodManufacturers/manufaacturers.service.spec.ts
new file mode 100644
index 000000000..14693be6a
--- /dev/null
+++ b/apps/backend/src/foodManufacturers/manufaacturers.service.spec.ts
@@ -0,0 +1,292 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { FoodManufacturersService } from './manufacturers.service';
+import { getRepositoryToken } from '@nestjs/typeorm';
+import { FoodManufacturer } from './manufacturers.entity';
+import { NotFoundException } from '@nestjs/common';
+import { FoodManufacturerApplicationDto } from './dtos/manufacturer-application.dto';
+import { ApplicationStatus } from '../shared/types';
+import { testDataSource } from '../config/typeormTestDataSource';
+import { Donation } from '../donations/donations.entity';
+import { User } from '../users/users.entity';
+import { UsersService } from '../users/users.service';
+import { AuthService } from '../auth/auth.service';
+import { EmailsService } from '../emails/email.service';
+import { Pantry } from '../pantries/pantries.entity';
+import { Order } from '../orders/order.entity';
+import { FoodRequest } from '../foodRequests/request.entity';
+import { DonationItem } from '../donationItems/donationItems.entity';
+import { DonationService } from '../donations/donations.service';
+import { PantriesService } from '../pantries/pantries.service';
+import { mock } from 'jest-mock-extended';
+import { emailTemplates, SSF_PARTNER_EMAIL } from '../emails/emailTemplates';
+import { Allergen, DonateWastedFood, ManufacturerAttribute } from './types';
+
+jest.setTimeout(60000);
+
+const dto: FoodManufacturerApplicationDto = {
+ foodManufacturerName: 'Test Manufacturer',
+ foodManufacturerWebsite: 'https://testmanufacturer.com',
+ contactFirstName: 'Jane',
+ contactLastName: 'Doe',
+ contactEmail: 'jane.doe@example.com',
+ contactPhone: '555-555-5555',
+ unlistedProductAllergens: [Allergen.SHELLFISH, Allergen.TREE_NUTS],
+ facilityFreeAllergens: [Allergen.PEANUT, Allergen.FISH],
+ productsGlutenFree: false,
+ productsContainSulfites: false,
+ productsSustainableExplanation: 'none',
+ inKindDonations: false,
+ donateWastedFood: DonateWastedFood.ALWAYS,
+};
+
+const mockEmailsService = mock();
+
+describe('FoodManufacturersService', () => {
+ let service: FoodManufacturersService;
+
+ beforeAll(async () => {
+ mockEmailsService.sendEmails.mockResolvedValue(undefined);
+
+ if (!testDataSource.isInitialized) {
+ await testDataSource.initialize();
+ }
+ await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`);
+ await testDataSource.query(`CREATE SCHEMA public`);
+
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ FoodManufacturersService,
+ UsersService,
+ DonationService,
+ PantriesService,
+ {
+ provide: AuthService,
+ useValue: {
+ adminCreateUser: jest.fn().mockResolvedValue('test-sub'),
+ },
+ },
+ {
+ provide: EmailsService,
+ useValue: mockEmailsService,
+ },
+ {
+ provide: getRepositoryToken(FoodManufacturer),
+ useValue: testDataSource.getRepository(FoodManufacturer),
+ },
+ {
+ provide: getRepositoryToken(User),
+ useValue: testDataSource.getRepository(User),
+ },
+ {
+ provide: getRepositoryToken(Donation),
+ useValue: testDataSource.getRepository(Donation),
+ },
+ {
+ provide: getRepositoryToken(Pantry),
+ useValue: testDataSource.getRepository(Pantry),
+ },
+ {
+ provide: getRepositoryToken(Order),
+ useValue: testDataSource.getRepository(Order),
+ },
+ {
+ provide: getRepositoryToken(FoodRequest),
+ useValue: testDataSource.getRepository(FoodRequest),
+ },
+ {
+ provide: getRepositoryToken(DonationItem),
+ useValue: testDataSource.getRepository(DonationItem),
+ },
+ ],
+ }).compile();
+
+ service = module.get(FoodManufacturersService);
+ });
+
+ beforeEach(async () => {
+ mockEmailsService.sendEmails.mockClear();
+ await testDataSource.runMigrations();
+ });
+
+ afterEach(async () => {
+ await testDataSource.query(`DROP SCHEMA public CASCADE`);
+ await testDataSource.query(`CREATE SCHEMA public`);
+ });
+
+ afterAll(async () => {
+ if (testDataSource.isInitialized) {
+ await testDataSource.destroy();
+ }
+ });
+
+ it('service should be defined', () => {
+ expect(service).toBeDefined();
+ });
+
+ describe('findOne', () => {
+ it('returns manufacturer by existing ID', async () => {
+ const manufacturer = await service.findOne(1);
+ expect(manufacturer).toBeDefined();
+ expect(manufacturer.foodManufacturerId).toBe(1);
+ });
+
+ it('throws NotFoundException for missing ID', async () => {
+ await expect(service.findOne(9999)).rejects.toThrow(
+ new NotFoundException('Food Manufacturer 9999 not found'),
+ );
+ });
+ });
+
+ describe('getPendingManufacturers', () => {
+ it('returns manufacturers with pending status', async () => {
+ const pending = await service.getPendingManufacturers();
+ expect(pending.length).toBeGreaterThan(0);
+ expect(pending.every((m) => m.status === ApplicationStatus.PENDING)).toBe(
+ true,
+ );
+ });
+ });
+
+ describe('approve', () => {
+ it('approves a pending manufacturer', async () => {
+ const pending = await service.getPendingManufacturers();
+ const id = pending[0].foodManufacturerId;
+
+ await service.approve(id);
+
+ const approved = await service.findOne(id);
+ expect(approved.status).toBe(ApplicationStatus.APPROVED);
+ });
+
+ it('sends approval email to manufacturer representative', async () => {
+ const pending = await service.getPendingManufacturers();
+ const manufacturer = pending[0];
+ const id = manufacturer.foodManufacturerId;
+ const { subject, bodyHTML } = emailTemplates.pantryFmApplicationApproved({
+ name: manufacturer.foodManufacturerRepresentative.firstName,
+ });
+
+ await service.approve(id);
+
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ [manufacturer.foodManufacturerRepresentative.email],
+ subject,
+ bodyHTML,
+ );
+ });
+
+ it('throws when approving non-existent manufacturer', async () => {
+ await expect(service.approve(9999)).rejects.toThrow(
+ new NotFoundException('Food Manufacturer 9999 not found'),
+ );
+ });
+ });
+
+ describe('deny', () => {
+ it('denies a pending manufacturer', async () => {
+ const pending = await service.getPendingManufacturers();
+ const id = pending[0].foodManufacturerId;
+
+ await service.deny(id);
+
+ const denied = await service.findOne(id);
+ expect(denied.status).toBe(ApplicationStatus.DENIED);
+ });
+
+ it('throws when denying non-existent manufacturer', async () => {
+ await expect(service.deny(9999)).rejects.toThrow(
+ new NotFoundException('Food Manufacturer 9999 not found'),
+ );
+ });
+ });
+
+ describe('addFoodManufacturer', () => {
+ it('creates manufacturer with minimal required fields', async () => {
+ await service.addFoodManufacturer(dto);
+ const saved = await testDataSource
+ .getRepository(FoodManufacturer)
+ .findOne({
+ where: { foodManufacturerName: 'Test Manufacturer' },
+ relations: ['foodManufacturerRepresentative'],
+ });
+ expect(saved).toBeDefined();
+ expect(saved?.foodManufacturerRepresentative?.email).toBe(
+ 'jane.doe@example.com',
+ );
+ expect(saved?.status).toBe(ApplicationStatus.PENDING);
+ });
+
+ it('creates manufacturer with all optional fields included', async () => {
+ const optionalDto: FoodManufacturerApplicationDto = {
+ ...dto,
+ foodManufacturerName: 'Test Full Manufacturer',
+ contactEmail: 'john.smith@example.com',
+ secondaryContactFirstName: 'Sarah',
+ secondaryContactLastName: 'Johnson',
+ secondaryContactEmail: 'sarah.johnson@example.com',
+ secondaryContactPhone: '555-555-5557',
+ manufacturerAttribute: ManufacturerAttribute.ORGANIC,
+ additionalComments: 'We specialize in allergen-free products',
+ newsletterSubscription: true,
+ };
+
+ await service.addFoodManufacturer(optionalDto);
+ const saved = await testDataSource
+ .getRepository(FoodManufacturer)
+ .findOne({
+ where: { foodManufacturerName: 'Test Full Manufacturer' },
+ relations: ['foodManufacturerRepresentative'],
+ });
+ expect(saved).toBeDefined();
+ expect(saved?.foodManufacturerRepresentative?.email).toBe(
+ 'john.smith@example.com',
+ );
+ expect(saved?.status).toBe(ApplicationStatus.PENDING);
+ expect(saved?.secondaryContactFirstName).toBe('Sarah');
+ expect(saved?.manufacturerAttribute).toBe(ManufacturerAttribute.ORGANIC);
+ });
+
+ it('sends confirmation email to applicant and notification email to admin', async () => {
+ await service.addFoodManufacturer(dto);
+
+ const userMessage = emailTemplates.pantryFmApplicationSubmittedToUser({
+ name: dto.contactFirstName,
+ });
+ const adminMessage = emailTemplates.pantryFmApplicationSubmittedToAdmin();
+
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ [dto.contactEmail],
+ userMessage.subject,
+ userMessage.bodyHTML,
+ );
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ [SSF_PARTNER_EMAIL],
+ adminMessage.subject,
+ adminMessage.bodyHTML,
+ );
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('getFMDonations', () => {
+ it('returns donations for an existing manufacturer', async () => {
+ const donations = await service.getFMDonations(1);
+ expect(Array.isArray(donations)).toBe(true);
+ });
+
+ it('returns empty array for manufacturer with no donations', async () => {
+ await service.addFoodManufacturer(dto);
+ const saved = await testDataSource
+ .getRepository(FoodManufacturer)
+ .findOne({ where: { foodManufacturerName: 'Test Manufacturer' } });
+ const donations = await service.getFMDonations(saved!.foodManufacturerId);
+ expect(donations).toEqual([]);
+ });
+
+ it('throws NotFoundException for non-existent manufacturer', async () => {
+ await expect(service.getFMDonations(9999)).rejects.toThrow(
+ new NotFoundException('Food Manufacturer 9999 not found'),
+ );
+ });
+ });
+});
diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts
index f9675e56e..d4224b006 100644
--- a/apps/backend/src/foodRequests/request.service.spec.ts
+++ b/apps/backend/src/foodRequests/request.service.spec.ts
@@ -12,13 +12,19 @@ import { DonationItem } from '../donationItems/donationItems.entity';
import { testDataSource } from '../config/typeormTestDataSource';
import { NotFoundException } from '@nestjs/common';
import { EmailsService } from '../emails/email.service';
+import { mock } from 'jest-mock-extended';
+import { emailTemplates } from '../emails/emailTemplates';
jest.setTimeout(60000);
+const mockEmailsService = mock();
+
describe('RequestsService', () => {
let service: RequestsService;
beforeAll(async () => {
+ mockEmailsService.sendEmails.mockResolvedValue(undefined);
+
if (!testDataSource.isInitialized) {
await testDataSource.initialize();
}
@@ -26,7 +32,6 @@ describe('RequestsService', () => {
const module = await Test.createTestingModule({
providers: [
RequestsService,
- EmailsService,
{
provide: getRepositoryToken(FoodRequest),
useValue: testDataSource.getRepository(FoodRequest),
@@ -49,9 +54,7 @@ describe('RequestsService', () => {
},
{
provide: EmailsService,
- useValue: {
- sendEmails: jest.fn().mockResolvedValue(undefined),
- },
+ useValue: mockEmailsService,
},
],
}).compile();
@@ -60,6 +63,7 @@ describe('RequestsService', () => {
});
beforeEach(async () => {
+ mockEmailsService.sendEmails.mockClear();
await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`);
await testDataSource.query(`CREATE SCHEMA public`);
await testDataSource.runMigrations();
@@ -191,6 +195,31 @@ describe('RequestsService', () => {
expect(result.additionalInformation).toBeNull();
});
+ it('should send food request email to pantry volunteers', async () => {
+ const pantryId = 1;
+ const pantry = await testDataSource.getRepository(Pantry).findOne({
+ where: { pantryId },
+ relations: ['pantryUser', 'volunteers'],
+ });
+
+ await service.create(pantryId, RequestSize.MEDIUM, [
+ FoodType.DRIED_BEANS,
+ FoodType.REFRIGERATED_MEALS,
+ ]);
+
+ const { subject, bodyHTML } = emailTemplates.pantrySubmitsFoodRequest({
+ pantryName: pantry!.pantryName,
+ volunteerName: pantry!.pantryUser.firstName,
+ });
+ const volunteerEmails = (pantry!.volunteers ?? []).map((v) => v.email);
+
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ volunteerEmails,
+ subject,
+ bodyHTML,
+ );
+ });
+
it('should throw NotFoundException for non-existent pantry', async () => {
await expect(
service.create(
diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts
index 085399198..6ec18e119 100644
--- a/apps/backend/src/pantries/pantries.service.spec.ts
+++ b/apps/backend/src/pantries/pantries.service.spec.ts
@@ -28,6 +28,8 @@ import { FoodManufacturersService } from '../foodManufacturers/manufacturers.ser
import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity';
import { User } from '../users/users.entity';
import { EmailsService } from '../emails/email.service';
+import { mock } from 'jest-mock-extended';
+import { emailTemplates, SSF_PARTNER_EMAIL } from '../emails/emailTemplates';
jest.setTimeout(60000);
@@ -59,10 +61,40 @@ const makePantryDto = (i: number): PantryApplicationDto =>
needMoreOptions: 'none',
} as PantryApplicationDto);
+const dto: PantryApplicationDto = {
+ contactFirstName: 'Jane',
+ contactLastName: 'Doe',
+ contactEmail: 'jane.doe@example.com',
+ contactPhone: '555-555-5555',
+ hasEmailContact: true,
+ pantryName: 'Test Pantry',
+ shipmentAddressLine1: '1 Test St',
+ shipmentAddressCity: 'Testville',
+ shipmentAddressState: 'TX',
+ shipmentAddressZip: '11111',
+ mailingAddressLine1: '1 Test St',
+ mailingAddressCity: 'Testville',
+ mailingAddressState: 'TX',
+ mailingAddressZip: '11111',
+ allergenClients: 'none',
+ restrictions: ['none'],
+ refrigeratedDonation: RefrigeratedDonation.NO,
+ acceptFoodDeliveries: false,
+ reserveFoodForAllergic: ReserveFoodForAllergic.NO,
+ dedicatedAllergyFriendly: false,
+ activities: [Activity.CREATE_LABELED_SHELF],
+ itemsInStock: 'none',
+ needMoreOptions: 'none',
+};
+
+const mockEmailsService = mock();
+
describe('PantriesService', () => {
let service: PantriesService;
beforeAll(async () => {
+ mockEmailsService.sendEmails.mockResolvedValue(undefined);
+
if (!testDataSource.isInitialized) {
await testDataSource.initialize();
}
@@ -78,7 +110,6 @@ describe('PantriesService', () => {
DonationItemsService,
DonationService,
FoodManufacturersService,
- EmailsService,
{
provide: AuthService,
useValue: {
@@ -87,9 +118,7 @@ describe('PantriesService', () => {
},
{
provide: EmailsService,
- useValue: {
- sendEmails: jest.fn().mockResolvedValue(undefined),
- },
+ useValue: mockEmailsService,
},
{
provide: getRepositoryToken(Pantry),
@@ -126,6 +155,7 @@ describe('PantriesService', () => {
});
beforeEach(async () => {
+ mockEmailsService.sendEmails.mockClear();
await testDataSource.runMigrations();
});
@@ -177,6 +207,21 @@ describe('PantriesService', () => {
expect(pantryAfter.status).toBe(ApplicationStatus.APPROVED);
});
+ it('sends approval email to pantry user', async () => {
+ const pantry = await service.findOne(5);
+ const { subject, bodyHTML } = emailTemplates.pantryFmApplicationApproved({
+ name: pantry.pantryUser.firstName,
+ });
+
+ await service.approve(5);
+
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ [pantry.pantryUser.email],
+ subject,
+ bodyHTML,
+ );
+ });
+
it('throws when approving non-existent', async () => {
await expect(service.approve(9999)).rejects.toThrow(
new NotFoundException('Pantry 9999 not found'),
@@ -202,35 +247,9 @@ describe('PantriesService', () => {
describe('addPantry', () => {
it('creates pantry with minimal required fields', async () => {
- const dto: PantryApplicationDto = {
- contactFirstName: 'Jane',
- contactLastName: 'Doe',
- contactEmail: 'jane.doe@example.com',
- contactPhone: '555-555-5555',
- hasEmailContact: true,
- pantryName: 'Test Minimal Pantry',
- shipmentAddressLine1: '1 Test St',
- shipmentAddressCity: 'Testville',
- shipmentAddressState: 'TX',
- shipmentAddressZip: '11111',
- mailingAddressLine1: '1 Test St',
- mailingAddressCity: 'Testville',
- mailingAddressState: 'TX',
- mailingAddressZip: '11111',
- allergenClients: 'none',
- restrictions: ['none'],
- refrigeratedDonation: RefrigeratedDonation.NO,
- acceptFoodDeliveries: false,
- reserveFoodForAllergic: ReserveFoodForAllergic.NO,
- dedicatedAllergyFriendly: false,
- activities: [Activity.CREATE_LABELED_SHELF],
- itemsInStock: 'none',
- needMoreOptions: 'none',
- };
-
await service.addPantry(dto);
const saved = await testDataSource.getRepository(Pantry).findOne({
- where: { pantryName: 'Test Minimal Pantry' },
+ where: { pantryName: 'Test Pantry' },
relations: ['pantryUser'],
});
expect(saved).toBeDefined();
@@ -239,32 +258,19 @@ describe('PantriesService', () => {
});
it('creates pantry with all optional fields included', async () => {
- const dto: PantryApplicationDto = {
- contactFirstName: 'John',
- contactLastName: 'Smith',
+ const optionalDto: PantryApplicationDto = {
+ ...dto,
+ pantryName: 'Test Full Pantry',
contactEmail: 'john.smith@example.com',
- contactPhone: '555-555-5556',
- hasEmailContact: true,
emailContactOther: 'Use work phone',
secondaryContactFirstName: 'Sarah',
secondaryContactLastName: 'Johnson',
secondaryContactEmail: 'sarah.johnson@example.com',
secondaryContactPhone: '555-555-5557',
- pantryName: 'Test Full Pantry',
- shipmentAddressLine1: '100 Main St',
shipmentAddressLine2: 'Suite 200',
- shipmentAddressCity: 'Springfield',
- shipmentAddressState: 'IL',
- shipmentAddressZip: '62701',
shipmentAddressCountry: 'USA',
- mailingAddressLine1: '100 Main St',
mailingAddressLine2: 'Suite 200',
- mailingAddressCity: 'Springfield',
- mailingAddressState: 'IL',
- mailingAddressZip: '62701',
mailingAddressCountry: 'USA',
- allergenClients: '10 to 20',
- restrictions: ['Peanut allergy', 'Tree nut allergy'],
refrigeratedDonation: RefrigeratedDonation.YES,
acceptFoodDeliveries: true,
deliveryWindowInstructions: 'Weekdays 9am-5pm',
@@ -276,12 +282,10 @@ describe('PantriesService', () => {
serveAllergicChildren: ServeAllergicChildren.YES_MANY,
activities: [Activity.CREATE_LABELED_SHELF, Activity.COLLECT_FEEDBACK],
activitiesComments: 'We are committed to allergen management',
- itemsInStock: 'Canned goods, pasta',
- needMoreOptions: 'Fresh produce',
newsletterSubscription: true,
- } as PantryApplicationDto;
+ };
- await service.addPantry(dto);
+ await service.addPantry(optionalDto);
const saved = await testDataSource.getRepository(Pantry).findOne({
where: { pantryName: 'Test Full Pantry' },
relations: ['pantryUser'],
@@ -292,6 +296,27 @@ describe('PantriesService', () => {
expect(saved?.secondaryContactFirstName).toBe('Sarah');
expect(saved?.shipmentAddressLine2).toBe('Suite 200');
});
+
+ it('sends confirmation email to applicant and notification email to admin', async () => {
+ await service.addPantry(dto);
+
+ const userMessage = emailTemplates.pantryFmApplicationSubmittedToUser({
+ name: dto.contactFirstName,
+ });
+ const adminMessage = emailTemplates.pantryFmApplicationSubmittedToAdmin();
+
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ [dto.contactEmail],
+ userMessage.subject,
+ userMessage.bodyHTML,
+ );
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ [SSF_PARTNER_EMAIL],
+ adminMessage.subject,
+ adminMessage.bodyHTML,
+ );
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2);
+ });
});
describe('getPantryStats (single pantry)', () => {
@@ -455,7 +480,6 @@ describe('PantriesService', () => {
});
it('validates all names before paginating — throws if any name is invalid regardless of page', async () => {
- // Create 12 valid pantries so we have enough to paginate
for (let i = 0; i < 12; i++) {
await service.addPantry(makePantryDto(i));
}
diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts
index a488bd573..db2035377 100644
--- a/apps/backend/src/users/users.service.spec.ts
+++ b/apps/backend/src/users/users.service.spec.ts
@@ -12,11 +12,12 @@ import { PantriesService } from '../pantries/pantries.service';
import { userSchemaDto } from './dtos/userSchema.dto';
import { AuthService } from '../auth/auth.service';
import { EmailsService } from '../emails/email.service';
+import { emailTemplates } from '../emails/emailTemplates';
const mockUserRepository = mock>();
const mockPantriesService = mock();
const mockAuthService = mock();
-
+const mockEmailsService = mock();
const mockUser: Partial = {
id: 1,
email: 'test@example.com',
@@ -56,9 +57,7 @@ describe('UsersService', () => {
},
{
provide: EmailsService,
- useValue: {
- sendEmails: jest.fn().mockResolvedValue(undefined),
- },
+ useValue: mockEmailsService,
},
],
}).compile();
@@ -84,6 +83,33 @@ describe('UsersService', () => {
});
describe('create', () => {
+ it('should send a welcome email when creating a volunteer', async () => {
+ const createUserDto: userSchemaDto = {
+ email: 'volunteer@example.com',
+ firstName: 'Jane',
+ lastName: 'Smith',
+ phone: '9876543210',
+ role: Role.VOLUNTEER,
+ };
+
+ const createdUser = {
+ ...createUserDto,
+ id: 2,
+ userCognitoSub: 'mock-sub',
+ } as User;
+ mockUserRepository.create.mockReturnValue(createdUser);
+ mockUserRepository.save.mockResolvedValue(createdUser);
+
+ await service.create(createUserDto);
+ const { subject, bodyHTML } = emailTemplates.volunteerAccountCreated();
+
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ [createUserDto.email],
+ subject,
+ bodyHTML,
+ );
+ });
+
it('should create a new user with auto-generated ID', async () => {
const createUserDto: userSchemaDto = {
email: 'newuser@example.com',
From f24b9f7c379c0b5d3195b1332007cd7a01fe343a Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
Date: Sun, 15 Mar 2026 00:09:47 -0400
Subject: [PATCH 6/6] Fixed CI
---
.github/workflows/backend-tests.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml
index 26b4e0f44..09c687ecc 100644
--- a/.github/workflows/backend-tests.yml
+++ b/.github/workflows/backend-tests.yml
@@ -33,6 +33,7 @@ jobs:
DATABASE_NAME_TEST: securing-safe-food-test
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: postgres
+ SEND_AUTOMATED_EMAILS: 'true'
NX_DAEMON: 'false'
CYPRESS_INSTALL_BINARY: '0'
steps: