From ddbe3fb40fb7c03f99bce2ddfbdc746de34beca6 Mon Sep 17 00:00:00 2001
From: Justin Wang
Date: Sun, 12 Apr 2026 23:01:44 -0400
Subject: [PATCH 1/5] email templates
---
apps/backend/src/emails/emailTemplates.ts | 97 +++++++++++++++++++++++
1 file changed, 97 insertions(+)
diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts
index 7d4b8c0f3..4ffb4739f 100644
--- a/apps/backend/src/emails/emailTemplates.ts
+++ b/apps/backend/src/emails/emailTemplates.ts
@@ -98,4 +98,101 @@ export const emailTemplates = {
Best regards,
The Securing Safe Food Team
`,
}),
+
+ pantryRequestMatchedOrder: (params: {
+ pantryName: string;
+ quantity: string;
+ product: string;
+ brand: string;
+ volunteerName: string;
+ volunteerEmail: string;
+ }): EmailTemplate => ({
+ subject: 'Your Securing Safe Food Request Has Been Matched to a Delivery',
+ bodyHTML: `
+ Hi ${params.pantryName},
+
+ Good news! Your recent food request through Securing Safe Food has been successfully matched to an order and is now moving forward toward delivery.
+ You can expect to receive ${params.quantity} of ${params.product} from ${params.brand}.
+ To view full order details, delivery updates, and any notes from the coordinating volunteer or food manufacturer, please log into the platform.
+
+
+ If any details change on your end or you have updated availability, please update your request in the system or email your coordinator, ${params.volunteerName} at ${params.volunteerEmail}.
+
+
+ We will continue to keep you informed as the order progresses. We’re excited to help support your pantry and looking forward to this donation!
+
+ Best regards,
The Securing Safe Food Team
+
+ To log in to your account, please click the following link: ${EMAIL_REDIRECT_URL}/login
+
+ `,
+ }),
+
+ fmDonationMatchedOrder: (params: {
+ manufacturerName: string;
+ quantity: string;
+ product: string;
+ pantryName: string;
+ pantryAddress: string;
+ volunteerName: string;
+ volunteerEmail: string;
+ }): EmailTemplate => ({
+ subject:
+ 'Your Securing Safe Food Donation Has Been Matched to a Pantry Order',
+ bodyHTML: `
+ Hi ${params.manufacturerName},
+
+ Thank you for your continued partnership with Securing Safe Food. A donation you submitted has now been successfully matched to a pantry request and is moving forward towards fulfillment.
+
+
+ Matched Item: ${params.quantity} of ${params.product}
+ Recipient Pantry: ${params.pantryName}
+ Address:
+ ${params.pantryAddress}
+
+
+ Please log into the platform to review the full delivery details, timelines, and any special handling instructions associated with this shipment.
+
+
+ Your support plays a direct role in expanding access to allergen-safe foods, and we truly appreciate your commitment to this work.
+
+
+ If you have any questions or need assistance, please contact your coordinator, ${params.volunteerName} at ${params.volunteerEmail}.
+
+
+ Thank you so much.
+
+ Best regards,
The Securing Safe Food Team
+
+ To log in to your account, please click the following link: ${EMAIL_REDIRECT_URL}/login
+
+ `,
+ }),
+
+ trackingLinkAvailable: (params: {
+ pantryName: string;
+ manufacturerName: string;
+ trackingLink: string;
+ volunteerName: string;
+ volunteerEmail: string;
+ }): EmailTemplate => ({
+ subject: `Tracking Information for your ${params.manufacturerName} delivery (Securing Safe Food)`,
+ bodyHTML: `
+ Hi ${params.pantryName},
+
+ Good news! Tracking information is now available for your upcoming SSF delivery from ${params.manufacturerName}. You can use this tracking information to monitor the status of your shipment or log into your portal for more information on your expected donation.
+
+
+ Tracking Link:
+ ${params.trackingLink}
+
+
+ You can use the tracking link above to monitor your shipment, or log into your portal for full order details and updates.
+
+
+ If you experience any issues or have questions, please contact your coordinator, ${params.volunteerName}, at ${params.volunteerEmail}, and our team will be happy to assist.
+
+ Best regards,
The Securing Safe Food Team
+ `,
+ }),
};
From 4058b0316ef7fe20cdae872a564b57cc076631f8 Mon Sep 17 00:00:00 2001
From: Justin Wang
Date: Thu, 16 Apr 2026 11:01:33 -0400
Subject: [PATCH 2/5] multiple items for email template
---
apps/backend/src/emails/emailTemplates.ts | 31 +++++++++++++++++------
1 file changed, 23 insertions(+), 8 deletions(-)
diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts
index 4ffb4739f..e291c54aa 100644
--- a/apps/backend/src/emails/emailTemplates.ts
+++ b/apps/backend/src/emails/emailTemplates.ts
@@ -101,8 +101,7 @@ export const emailTemplates = {
pantryRequestMatchedOrder: (params: {
pantryName: string;
- quantity: string;
- product: string;
+ items: { quantity: string; product: string }[];
brand: string;
volunteerName: string;
volunteerEmail: string;
@@ -112,11 +111,22 @@ export const emailTemplates = {
Hi ${params.pantryName},
Good news! Your recent food request through Securing Safe Food has been successfully matched to an order and is now moving forward toward delivery.
- You can expect to receive ${params.quantity} of ${params.product} from ${params.brand}.
+
+
+ Items you will receive from ${params.brand}:
+
+ ${params.items
+ .map((item) => `- ${item.quantity} of ${item.product}
`)
+ .join('')}
+
+
+
To view full order details, delivery updates, and any notes from the coordinating volunteer or food manufacturer, please log into the platform.
- If any details change on your end or you have updated availability, please update your request in the system or email your coordinator, ${params.volunteerName} at ${params.volunteerEmail}.
+ If any details change on your end or you have updated availability, please update your request in the system or email your coordinator, ${
+ params.volunteerName
+ } at ${params.volunteerEmail}.
We will continue to keep you informed as the order progresses. We’re excited to help support your pantry and looking forward to this donation!
@@ -130,8 +140,7 @@ export const emailTemplates = {
fmDonationMatchedOrder: (params: {
manufacturerName: string;
- quantity: string;
- product: string;
+ items: { quantity: string; product: string }[];
pantryName: string;
pantryAddress: string;
volunteerName: string;
@@ -145,7 +154,11 @@ export const emailTemplates = {
Thank you for your continued partnership with Securing Safe Food. A donation you submitted has now been successfully matched to a pantry request and is moving forward towards fulfillment.
- Matched Item: ${params.quantity} of ${params.product}
+ Matched Items:
+ ${params.items
+ .map((item) => `• ${item.quantity} of ${item.product}`)
+ .join('
')}
+
Recipient Pantry: ${params.pantryName}
Address:
${params.pantryAddress}
@@ -157,7 +170,9 @@ export const emailTemplates = {
Your support plays a direct role in expanding access to allergen-safe foods, and we truly appreciate your commitment to this work.
- If you have any questions or need assistance, please contact your coordinator, ${params.volunteerName} at ${params.volunteerEmail}.
+ If you have any questions or need assistance, please contact your coordinator, ${
+ params.volunteerName
+ } at ${params.volunteerEmail}.
Thank you so much.
From 61f6859f26e74f3cac44782d66368af10f89265c Mon Sep 17 00:00:00 2001
From: Justin Wang
Date: Sun, 19 Apr 2026 22:30:14 -0400
Subject: [PATCH 3/5] logic for sending emails
---
apps/backend/src/emails/emailTemplates.ts | 27 -------
apps/backend/src/orders/order.module.ts | 2 +
apps/backend/src/orders/order.service.spec.ts | 10 +++
apps/backend/src/orders/order.service.ts | 70 ++++++++++++++++++-
4 files changed, 81 insertions(+), 28 deletions(-)
diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts
index e291c54aa..d1551da1e 100644
--- a/apps/backend/src/emails/emailTemplates.ts
+++ b/apps/backend/src/emails/emailTemplates.ts
@@ -183,31 +183,4 @@ export const emailTemplates = {
`,
}),
-
- trackingLinkAvailable: (params: {
- pantryName: string;
- manufacturerName: string;
- trackingLink: string;
- volunteerName: string;
- volunteerEmail: string;
- }): EmailTemplate => ({
- subject: `Tracking Information for your ${params.manufacturerName} delivery (Securing Safe Food)`,
- bodyHTML: `
- Hi ${params.pantryName},
-
- Good news! Tracking information is now available for your upcoming SSF delivery from ${params.manufacturerName}. You can use this tracking information to monitor the status of your shipment or log into your portal for more information on your expected donation.
-
-
- Tracking Link:
- ${params.trackingLink}
-
-
- You can use the tracking link above to monitor your shipment, or log into your portal for full order details and updates.
-
-
- If you experience any issues or have questions, please contact your coordinator, ${params.volunteerName}, at ${params.volunteerEmail}, and our team will be happy to assist.
-
- Best regards,
The Securing Safe Food Team
- `,
- }),
};
diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts
index 71003cc7e..07b33f5fa 100644
--- a/apps/backend/src/orders/order.module.ts
+++ b/apps/backend/src/orders/order.module.ts
@@ -17,6 +17,7 @@ import { DonationItemsModule } from '../donationItems/donationItems.module';
import { Allocation } from '../allocations/allocations.entity';
import { DonationModule } from '../donations/donations.module';
import { Donation } from '../donations/donations.entity';
+import { EmailsModule } from '../emails/email.module';
@Module({
imports: [
@@ -37,6 +38,7 @@ import { Donation } from '../donations/donations.entity';
ManufacturerModule,
DonationItemsModule,
DonationModule,
+ EmailsModule,
],
controllers: [OrdersController],
providers: [OrdersService],
diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts
index fea6912dd..ea210a9d1 100644
--- a/apps/backend/src/orders/order.service.spec.ts
+++ b/apps/backend/src/orders/order.service.spec.ts
@@ -28,14 +28,19 @@ import { DonationStatus } from '../donations/types';
import { DataSource } from 'typeorm';
import { EmailsService } from '../emails/email.service';
import { Allocation } from '../allocations/allocations.entity';
+import { mock } from 'jest-mock-extended';
// Set 1 minute timeout for async DB operations
jest.setTimeout(60000);
+const mockEmailsService = mock();
+
describe('OrdersService', () => {
let service: OrdersService;
beforeAll(async () => {
+ mockEmailsService.sendEmails.mockResolvedValue(undefined);
+
// Initialize DataSource once
if (!testDataSource.isInitialized) {
await testDataSource.initialize();
@@ -101,6 +106,10 @@ describe('OrdersService', () => {
provide: AuthService,
useValue: {},
},
+ {
+ provide: EmailsService,
+ useValue: mockEmailsService,
+ },
],
}).compile();
@@ -108,6 +117,7 @@ describe('OrdersService', () => {
});
beforeEach(async () => {
+ mockEmailsService.sendEmails.mockClear();
await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`);
await testDataSource.query(`CREATE SCHEMA public`);
await testDataSource.runMigrations();
diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts
index 378718e0e..1099e3581 100644
--- a/apps/backend/src/orders/order.service.ts
+++ b/apps/backend/src/orders/order.service.ts
@@ -1,6 +1,7 @@
import {
BadRequestException,
Injectable,
+ InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
@@ -23,6 +24,9 @@ import { DonationService } from '../donations/donations.service';
import { ApplicationStatus } from '../shared/types';
import { Donation } from '../donations/donations.entity';
import { VolunteerOrder } from '../volunteers/types';
+import { EmailsService } from '../emails/email.service';
+import { FoodRequest } from '../foodRequests/request.entity';
+import { emailTemplates } from '../emails/emailTemplates';
@Injectable()
export class OrdersService {
@@ -30,11 +34,13 @@ export class OrdersService {
@InjectRepository(Order) private repo: Repository,
@InjectRepository(Pantry) private pantryRepo: Repository,
@InjectRepository(Donation) private donationRepo: Repository,
+ @InjectRepository(FoodRequest) private requestRepo: Repository,
private requestsService: RequestsService,
private manufacturerService: FoodManufacturersService,
private donationItemsService: DonationItemsService,
private allocationsService: AllocationsService,
private donationService: DonationService,
+ private emailsService: EmailsService,
@InjectDataSource() private dataSource: DataSource,
) {}
@@ -151,7 +157,14 @@ export class OrdersService {
validateId(manufacturerId, 'Food Manufacturer');
validateId(requestId, 'Request');
- const request = await this.requestsService.findOne(requestId);
+ const request = await this.requestRepo.findOne({
+ where: { requestId },
+ relations: ['pantry', 'pantry.pantryUser'],
+ });
+
+ if (!request) {
+ throw new NotFoundException(`Request ${requestId} not found`);
+ }
if (request.status !== FoodRequestStatus.ACTIVE) {
throw new BadRequestException(`Request ${requestId} is not active`);
@@ -195,6 +208,8 @@ export class OrdersService {
);
}
+ const itemDetails: { quantity: string; product: string }[] = [];
+
for (const donationItem of donationItems) {
const id = donationItem.itemId;
const quantityToAllocate = itemAllocations.get(id)!;
@@ -207,6 +222,11 @@ export class OrdersService {
`Donation item ${id} quantity to allocate exceeds remaining quantity`,
);
}
+
+ itemDetails.push({
+ quantity: String(quantityToAllocate),
+ product: donationItem.itemName,
+ });
}
const orderTransactionRepo = transactionManager.getRepository(Order);
@@ -236,6 +256,54 @@ export class OrdersService {
transactionManager,
);
+ try {
+ const pantryMessage = emailTemplates.pantryRequestMatchedOrder({
+ pantryName: request.pantry.pantryName,
+ items: itemDetails,
+ brand: manufacturer.foodManufacturerName,
+ volunteerName: '',
+ volunteerEmail: '',
+ });
+ await this.emailsService.sendEmails(
+ [request.pantry.pantryUser.email],
+ pantryMessage.subject,
+ pantryMessage.bodyHTML,
+ );
+ } catch {
+ throw new InternalServerErrorException(
+ 'Failed to send pantry request matched order confirmation email to representative',
+ );
+ }
+
+ try {
+ const fmMessage = emailTemplates.fmDonationMatchedOrder({
+ manufacturerName: manufacturer.foodManufacturerName,
+ items: itemDetails,
+ pantryName: request.pantry.pantryName,
+ pantryAddress:
+ request.pantry.mailingAddressLine1 +
+ ', ' +
+ request.pantry.mailingAddressCity +
+ ', ' +
+ request.pantry.mailingAddressState +
+ ' ' +
+ request.pantry.mailingAddressZip +
+ ' ' +
+ request.pantry.mailingAddressCountry,
+ volunteerName: '',
+ volunteerEmail: '',
+ });
+ await this.emailsService.sendEmails(
+ [manufacturer.foodManufacturerRepresentative.email],
+ fmMessage.subject,
+ fmMessage.bodyHTML,
+ );
+ } catch {
+ throw new InternalServerErrorException(
+ 'Failed to send food manufacturer donation matched to order confirmation email to representative',
+ );
+ }
+
return savedOrder;
});
}
From 83baa1ccc56aea4867d49c144440d393ec43abdf Mon Sep 17 00:00:00 2001
From: Justin Wang
Date: Tue, 21 Apr 2026 10:51:55 -0400
Subject: [PATCH 4/5] tests
---
apps/backend/src/orders/order.module.ts | 4 ++
apps/backend/src/orders/order.service.spec.ts | 68 ++++++++++++++++++-
apps/backend/src/orders/order.service.ts | 16 +++--
3 files changed, 81 insertions(+), 7 deletions(-)
diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts
index 07b33f5fa..8f2231162 100644
--- a/apps/backend/src/orders/order.module.ts
+++ b/apps/backend/src/orders/order.module.ts
@@ -18,6 +18,8 @@ import { Allocation } from '../allocations/allocations.entity';
import { DonationModule } from '../donations/donations.module';
import { Donation } from '../donations/donations.entity';
import { EmailsModule } from '../emails/email.module';
+import { User } from '../users/users.entity';
+import { UsersModule } from '../users/users.module';
@Module({
imports: [
@@ -29,6 +31,7 @@ import { EmailsModule } from '../emails/email.module';
DonationItem,
Allocation,
Donation,
+ User,
]),
AllocationModule,
forwardRef(() => AuthModule),
@@ -39,6 +42,7 @@ import { EmailsModule } from '../emails/email.module';
DonationItemsModule,
DonationModule,
EmailsModule,
+ forwardRef(() => UsersModule),
],
controllers: [OrdersController],
providers: [OrdersService],
diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts
index ea210a9d1..47e3d4334 100644
--- a/apps/backend/src/orders/order.service.spec.ts
+++ b/apps/backend/src/orders/order.service.spec.ts
@@ -29,6 +29,7 @@ import { DataSource } from 'typeorm';
import { EmailsService } from '../emails/email.service';
import { Allocation } from '../allocations/allocations.entity';
import { mock } from 'jest-mock-extended';
+import { emailTemplates } from '../emails/emailTemplates';
// Set 1 minute timeout for async DB operations
jest.setTimeout(60000);
@@ -754,10 +755,13 @@ describe('OrdersService', () => {
]);
});
- it('should create a new order successfully', async () => {
+ it('should create a new order successfully and send appropriate emails', async () => {
const allocationRepo = testDataSource.getRepository(Allocation);
const donationItemRepo = testDataSource.getRepository(DonationItem);
const donationRepo = testDataSource.getRepository(Donation);
+ const usersRepo = testDataSource.getRepository(User);
+ const requestRepo = testDataSource.getRepository(FoodRequest);
+ const manufacturerRepo = testDataSource.getRepository(FoodManufacturer);
parsedAllocations.set(9, 5);
@@ -835,6 +839,68 @@ describe('OrdersService', () => {
where: { donationId: 2 },
});
expect(matchedDonation2?.status).toBe(DonationStatus.MATCHED);
+
+ // Testing emails section
+
+ const assignee = await usersRepo.findOne({ where: { id: userId } });
+ const request = await requestRepo.findOne({
+ where: { requestId: validCreateOrderDto.foodRequestId },
+ relations: ['pantry', 'pantry.pantryUser'],
+ });
+ const manufacturer = await manufacturerRepo.findOne({
+ where: { foodManufacturerId: validCreateOrderDto.manufacturerId },
+ relations: ['foodManufacturerRepresentative'],
+ });
+
+ const pantry = request!.pantry;
+ const pantryAddress = [
+ pantry.mailingAddressLine1,
+ pantry.mailingAddressCity,
+ pantry.mailingAddressState,
+ pantry.mailingAddressZip,
+ pantry.mailingAddressCountry,
+ ]
+ .join(' ')
+ .replace(/, ,/g, ', ');
+
+ const itemDetails = [
+ { quantity: '10', product: updatedDonationItem1!.itemName },
+ { quantity: '3', product: updatedDonationItem2!.itemName },
+ { quantity: '5', product: updatedDonationItem3!.itemName },
+ ];
+
+ const { subject: fmSubject, bodyHTML: fmBodyHtml } =
+ emailTemplates.fmDonationMatchedOrder({
+ manufacturerName: manufacturer!.foodManufacturerName,
+ items: itemDetails,
+ pantryName: pantry.pantryName,
+ pantryAddress,
+ volunteerName: assignee!.firstName + ' ' + assignee!.lastName,
+ volunteerEmail: assignee!.email,
+ });
+
+ const { subject: pantrySubject, bodyHTML: pantryBodyHtml } =
+ emailTemplates.pantryRequestMatchedOrder({
+ pantryName: request!.pantry.pantryName,
+ items: itemDetails,
+ brand: manufacturer!.foodManufacturerName,
+ volunteerName: assignee!.firstName + ' ' + assignee!.lastName,
+ volunteerEmail: assignee!.email,
+ });
+
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2);
+
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ [request!.pantry.pantryUser.email],
+ pantrySubject,
+ pantryBodyHtml,
+ );
+
+ expect(mockEmailsService.sendEmails).toHaveBeenCalledWith(
+ [manufacturer!.foodManufacturerRepresentative.email],
+ fmSubject,
+ fmBodyHtml,
+ );
});
it('should throw BadRequestException if request is not active', async () => {
diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts
index 1099e3581..a420902bb 100644
--- a/apps/backend/src/orders/order.service.ts
+++ b/apps/backend/src/orders/order.service.ts
@@ -27,6 +27,7 @@ import { VolunteerOrder } from '../volunteers/types';
import { EmailsService } from '../emails/email.service';
import { FoodRequest } from '../foodRequests/request.entity';
import { emailTemplates } from '../emails/emailTemplates';
+import { UsersService } from '../users/users.service';
@Injectable()
export class OrdersService {
@@ -36,6 +37,7 @@ export class OrdersService {
@InjectRepository(Donation) private donationRepo: Repository,
@InjectRepository(FoodRequest) private requestRepo: Repository,
private requestsService: RequestsService,
+ private usersService: UsersService,
private manufacturerService: FoodManufacturersService,
private donationItemsService: DonationItemsService,
private allocationsService: AllocationsService,
@@ -256,13 +258,15 @@ export class OrdersService {
transactionManager,
);
+ const assignee = await this.usersService.findOne(userId);
+
try {
const pantryMessage = emailTemplates.pantryRequestMatchedOrder({
pantryName: request.pantry.pantryName,
items: itemDetails,
brand: manufacturer.foodManufacturerName,
- volunteerName: '',
- volunteerEmail: '',
+ volunteerName: assignee.firstName + ' ' + assignee.lastName,
+ volunteerEmail: assignee.email,
});
await this.emailsService.sendEmails(
[request.pantry.pantryUser.email],
@@ -282,16 +286,16 @@ export class OrdersService {
pantryName: request.pantry.pantryName,
pantryAddress:
request.pantry.mailingAddressLine1 +
- ', ' +
+ ' ' +
request.pantry.mailingAddressCity +
- ', ' +
+ ' ' +
request.pantry.mailingAddressState +
' ' +
request.pantry.mailingAddressZip +
' ' +
request.pantry.mailingAddressCountry,
- volunteerName: '',
- volunteerEmail: '',
+ volunteerName: assignee.firstName + ' ' + assignee.lastName,
+ volunteerEmail: assignee.email,
});
await this.emailsService.sendEmails(
[manufacturer.foodManufacturerRepresentative.email],
From 5deb81b96bea34ed33411744703cf6cebf01b3ea Mon Sep 17 00:00:00 2001
From: Justin Wang
Date: Tue, 21 Apr 2026 11:08:19 -0400
Subject: [PATCH 5/5] fix bug
---
apps/backend/src/orders/order.service.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts
index c2154be58..d96e8e94b 100644
--- a/apps/backend/src/orders/order.service.ts
+++ b/apps/backend/src/orders/order.service.ts
@@ -37,7 +37,8 @@ export class OrdersService {
@InjectRepository(Pantry) private pantryRepo: Repository,
@InjectRepository(Donation) private donationRepo: Repository,
@InjectRepository(FoodRequest) private requestRepo: Repository,
- @InjectRepository(DonationItem) private donationItemRepo: Repository,
+ @InjectRepository(DonationItem)
+ private donationItemRepo: Repository,
private requestsService: RequestsService,
private usersService: UsersService,
private manufacturerService: FoodManufacturersService,
@@ -45,7 +46,6 @@ export class OrdersService {
private allocationsService: AllocationsService,
private donationService: DonationService,
private emailsService: EmailsService,
- private donationService: DonationService,
@InjectDataSource() private dataSource: DataSource,
) {}