From 55495f71daec8386965b38d532059f853f890883 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart 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, 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, 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, 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
The Securing Safe Food Team
The Securing Safe Food Team
The Securing Safe Food Team
Best regards,
The Securing Safe Food Team
Best regards,
The Securing Safe Food Team
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
Hi ${params.volunteerName},
+Hi,
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.
diff --git a/apps/backend/src/foodManufacturers/manufaacturers.service.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts
similarity index 89%
rename from apps/backend/src/foodManufacturers/manufaacturers.service.spec.ts
rename to apps/backend/src/foodManufacturers/manufacturers.service.spec.ts
index 14693be6a..47c5d94c1 100644
--- a/apps/backend/src/foodManufacturers/manufaacturers.service.spec.ts
+++ b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts
@@ -168,6 +168,7 @@ describe('FoodManufacturersService', () => {
await service.approve(id);
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1);
expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
[manufacturer.foodManufacturerRepresentative.email],
subject,
@@ -175,6 +176,19 @@ describe('FoodManufacturersService', () => {
);
});
+ it('should still update manufacturer status to approved if email send fails', async () => {
+ const pending = await service.getPendingManufacturers();
+ const id = pending[0].foodManufacturerId;
+ mockEmailsService.sendEmails.mockRejectedValueOnce(
+ new Error('Email failed'),
+ );
+
+ await expect(service.approve(id)).rejects.toThrow('Email failed');
+
+ const approved = await service.findOne(id);
+ expect(approved.status).toBe(ApplicationStatus.APPROVED);
+ });
+
it('throws when approving non-existent manufacturer', async () => {
await expect(service.approve(9999)).rejects.toThrow(
new NotFoundException('Food Manufacturer 9999 not found'),
@@ -191,6 +205,7 @@ describe('FoodManufacturersService', () => {
const denied = await service.findOne(id);
expect(denied.status).toBe(ApplicationStatus.DENIED);
+ expect(mockEmailsService.sendEmails).not.toHaveBeenCalled();
});
it('throws when denying non-existent manufacturer', async () => {
@@ -246,6 +261,25 @@ describe('FoodManufacturersService', () => {
expect(saved?.manufacturerAttribute).toBe(ManufacturerAttribute.ORGANIC);
});
+ it('should still save manufacturer to database if email send fails', async () => {
+ mockEmailsService.sendEmails.mockRejectedValueOnce(
+ new Error('Email failed'),
+ );
+
+ await expect(service.addFoodManufacturer(dto)).rejects.toThrow(
+ 'Email failed',
+ );
+
+ const saved = await testDataSource
+ .getRepository(FoodManufacturer)
+ .findOne({
+ where: { foodManufacturerName: 'Test Manufacturer' },
+ relations: ['foodManufacturerRepresentative'],
+ });
+ expect(saved).toBeDefined();
+ expect(saved?.status).toBe(ApplicationStatus.PENDING);
+ });
+
it('sends confirmation email to applicant and notification email to admin', async () => {
await service.addFoodManufacturer(dto);
diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts
index d4224b006..d8993ecb8 100644
--- a/apps/backend/src/foodRequests/request.service.spec.ts
+++ b/apps/backend/src/foodRequests/request.service.spec.ts
@@ -209,10 +209,10 @@ describe('RequestsService', () => {
const { subject, bodyHTML } = emailTemplates.pantrySubmitsFoodRequest({
pantryName: pantry!.pantryName,
- volunteerName: pantry!.pantryUser.firstName,
});
const volunteerEmails = (pantry!.volunteers ?? []).map((v) => v.email);
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1);
expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
volunteerEmails,
subject,
@@ -220,6 +220,47 @@ describe('RequestsService', () => {
);
});
+ it('should send emails to nobody if request creation succeeds wthout any volunteers', async () => {
+ // Harbor Community Center - no volunteers assigned
+ const pantryId = 5;
+ 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,
+ });
+ const volunteerEmails = (pantry!.volunteers ?? []).map((v) => v.email);
+
+ expect(volunteerEmails).toEqual([]);
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1);
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ volunteerEmails,
+ subject,
+ bodyHTML,
+ );
+ });
+
+ it('should still save food request to database if email send fails', async () => {
+ mockEmailsService.sendEmails.mockRejectedValueOnce(
+ new Error('Email failed'),
+ );
+
+ const pantryId = 1;
+ await expect(
+ service.create(pantryId, RequestSize.MEDIUM, [FoodType.DRIED_BEANS]),
+ ).rejects.toThrow('Email failed');
+
+ const requests = await service.find(pantryId);
+ expect(requests.length).toBe(3);
+ });
+
it('should throw NotFoundException for non-existent pantry', async () => {
await expect(
service.create(
diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts
index dc6b71162..6b250eb7a 100644
--- a/apps/backend/src/foodRequests/request.service.ts
+++ b/apps/backend/src/foodRequests/request.service.ts
@@ -216,12 +216,13 @@ export class RequestsService {
additionalInformation,
});
+ await this.repo.save(foodRequest);
+
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(
@@ -230,7 +231,7 @@ export class RequestsService {
message.bodyHTML,
);
- return await this.repo.save(foodRequest);
+ return foodRequest;
}
async find(pantryId: number) {
diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts
index 6ec18e119..e54217e10 100644
--- a/apps/backend/src/pantries/pantries.service.spec.ts
+++ b/apps/backend/src/pantries/pantries.service.spec.ts
@@ -215,6 +215,7 @@ describe('PantriesService', () => {
await service.approve(5);
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1);
expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
[pantry.pantryUser.email],
subject,
@@ -222,6 +223,17 @@ describe('PantriesService', () => {
);
});
+ it('should still update pantry status to approved if email send fails', async () => {
+ mockEmailsService.sendEmails.mockRejectedValueOnce(
+ new Error('Email failed'),
+ );
+
+ await expect(service.approve(5)).rejects.toThrow('Email failed');
+
+ const pantry = await service.findOne(5);
+ expect(pantry.status).toBe(ApplicationStatus.APPROVED);
+ });
+
it('throws when approving non-existent', async () => {
await expect(service.approve(9999)).rejects.toThrow(
new NotFoundException('Pantry 9999 not found'),
@@ -297,6 +309,21 @@ describe('PantriesService', () => {
expect(saved?.shipmentAddressLine2).toBe('Suite 200');
});
+ it('should still save pantry to database if email send fails', async () => {
+ mockEmailsService.sendEmails.mockRejectedValueOnce(
+ new Error('Email failed'),
+ );
+
+ await expect(service.addPantry(dto)).rejects.toThrow('Email failed');
+
+ const saved = await testDataSource.getRepository(Pantry).findOne({
+ where: { pantryName: 'Test Pantry' },
+ relations: ['pantryUser'],
+ });
+ expect(saved).toBeDefined();
+ expect(saved?.status).toBe(ApplicationStatus.PENDING);
+ });
+
it('sends confirmation email to applicant and notification email to admin', async () => {
await service.addPantry(dto);
diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts
index db2035377..939d72f03 100644
--- a/apps/backend/src/users/users.service.spec.ts
+++ b/apps/backend/src/users/users.service.spec.ts
@@ -103,6 +103,7 @@ describe('UsersService', () => {
await service.create(createUserDto);
const { subject, bodyHTML } = emailTemplates.volunteerAccountCreated();
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1);
expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
[createUserDto.email],
subject,
@@ -110,6 +111,33 @@ describe('UsersService', () => {
);
});
+ it('should still save user to database if email send fails', 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);
+ mockEmailsService.sendEmails.mockRejectedValueOnce(
+ new Error('Email failed'),
+ );
+
+ await expect(service.create(createUserDto)).rejects.toThrow(
+ 'Email failed',
+ );
+
+ expect(mockUserRepository.save).toHaveBeenCalledWith(createdUser);
+ });
+
it('should create a new user with auto-generated ID', async () => {
const createUserDto: userSchemaDto = {
email: 'newuser@example.com',
From c2620f7da4bbd3569e775a51ad4f008d4eb586c3 Mon Sep 17 00:00:00 2001
From: Dalton Burkhart
- Thank you for your continued support of our network and mission!. + Thank you for your continued support of our network and mission!
Best regards,
The Securing Safe Food Team