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}: +

+

+

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, ) {}