From ddbe3fb40fb7c03f99bce2ddfbdc746de34beca6 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Sun, 12 Apr 2026 23:01:44 -0400 Subject: [PATCH 01/20] 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 02/20] 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 03/20] 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 04/20] 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 05/20] 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, ) {} From 6006842805fbf5528edecd7049839fa90b7d8c0b Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Wed, 29 Apr 2026 13:22:34 -0400 Subject: [PATCH 06/20] comments --- apps/backend/src/emails/emailTemplates.ts | 9 +- apps/backend/src/orders/order.service.ts | 281 ++++++++++++---------- 2 files changed, 156 insertions(+), 134 deletions(-) diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts index d1551da1e..0afe30eed 100644 --- a/apps/backend/src/emails/emailTemplates.ts +++ b/apps/backend/src/emails/emailTemplates.ts @@ -155,10 +155,11 @@ export const emailTemplates = {

Matched Items:
- ${params.items - .map((item) => `• ${item.quantity} of ${item.product}`) - .join('
')} -

+

    + ${params.items + .map((item) => `
  • ${item.quantity} of ${item.product}
  • `) + .join('')} +
Recipient Pantry: ${params.pantryName}
Address:
${params.pantryAddress} diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index d96e8e94b..960cc2f13 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -1,8 +1,8 @@ import { BadRequestException, Injectable, - InternalServerErrorException, NotFoundException, + Logger, } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { Repository, In, DataSource } from 'typeorm'; @@ -32,6 +32,8 @@ import { UsersService } from '../users/users.service'; @Injectable() export class OrdersService { + private readonly logger = new Logger(OrdersService.name); + constructor( @InjectRepository(Order) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, @@ -158,161 +160,180 @@ export class OrdersService { itemAllocations: Map, userId: number, ): Promise { - return this.dataSource.transaction(async (transactionManager) => { - validateId(manufacturerId, 'Food Manufacturer'); - validateId(requestId, 'Request'); - - const request = await this.requestRepo.findOne({ - where: { requestId }, - relations: ['pantry', 'pantry.pantryUser'], - }); - - if (!request) { - throw new NotFoundException(`Request ${requestId} not found`); - } + const { savedOrder, request, manufacturer, assignee, itemDetails } = + await this.dataSource.transaction(async (transactionManager) => { + validateId(manufacturerId, 'Food Manufacturer'); + validateId(requestId, 'Request'); + + const request = await this.requestRepo.findOne({ + where: { requestId }, + relations: ['pantry', 'pantry.pantryUser'], + }); - if (request.status !== FoodRequestStatus.ACTIVE) { - throw new BadRequestException(`Request ${requestId} is not active`); - } + if (!request) { + throw new NotFoundException(`Request ${requestId} not found`); + } - const manufacturer = await this.manufacturerService.findOne( - manufacturerId, - ); + if (request.status !== FoodRequestStatus.ACTIVE) { + throw new BadRequestException(`Request ${requestId} is not active`); + } - if (manufacturer.status !== ApplicationStatus.APPROVED) { - throw new BadRequestException( - `Manufacturer ${manufacturerId} is not approved`, + const manufacturer = await this.manufacturerService.findOne( + manufacturerId, ); - } - const fmDonations = await this.donationRepo.find({ - where: { foodManufacturer: { foodManufacturerId: manufacturerId } }, - select: ['donationId'], - }); - - const fmDonationIdSet = new Set(fmDonations.map((d) => d.donationId)); + if (manufacturer.status !== ApplicationStatus.APPROVED) { + throw new BadRequestException( + `Manufacturer ${manufacturerId} is not approved`, + ); + } - const donationItemIds = Array.from(itemAllocations.keys()); - const donationItems = await this.donationItemsService.getByIds( - donationItemIds, - ); + const fmDonations = await this.donationRepo.find({ + where: { foodManufacturer: { foodManufacturerId: manufacturerId } }, + select: ['donationId'], + }); - const invalidItems = donationItems.filter( - (item) => !fmDonationIdSet.has(item.donationId), - ); + const fmDonationIdSet = new Set(fmDonations.map((d) => d.donationId)); - if (invalidItems.length > 0) { - const messages = invalidItems.map( - (item) => - `Donation item ID ${item.itemId} with Donation ID ${item.donationId}`, - ); - throw new BadRequestException( - `The following donation items are not associated with the current food manufacturer: ${messages.join( - ', ', - )}`, + const donationItemIds = Array.from(itemAllocations.keys()); + const donationItems = await this.donationItemsService.getByIds( + donationItemIds, ); - } - const itemDetails: { quantity: string; product: string }[] = []; - - for (const donationItem of donationItems) { - const id = donationItem.itemId; - const quantityToAllocate = itemAllocations.get(id)!; + const invalidItems = donationItems.filter( + (item) => !fmDonationIdSet.has(item.donationId), + ); - if ( - quantityToAllocate > - donationItem.quantity - donationItem.reservedQuantity - ) { + if (invalidItems.length > 0) { + const messages = invalidItems.map( + (item) => + `Donation item ID ${item.itemId} with Donation ID ${item.donationId}`, + ); throw new BadRequestException( - `Donation item ${id} quantity to allocate exceeds remaining quantity`, + `The following donation items are not associated with the current food manufacturer: ${messages.join( + ', ', + )}`, ); } - itemDetails.push({ - quantity: String(quantityToAllocate), - product: donationItem.itemName, - }); - } + const itemDetails: { quantity: string; product: string }[] = []; + + for (const donationItem of donationItems) { + const id = donationItem.itemId; + const quantityToAllocate = itemAllocations.get(id)!; + + if ( + quantityToAllocate > + donationItem.quantity - donationItem.reservedQuantity + ) { + throw new BadRequestException( + `Donation item ${id} quantity to allocate exceeds remaining quantity`, + ); + } + + itemDetails.push({ + quantity: String(quantityToAllocate), + product: donationItem.itemName, + }); + } - const orderTransactionRepo = transactionManager.getRepository(Order); + const orderTransactionRepo = transactionManager.getRepository(Order); - const order = orderTransactionRepo.create({ - requestId: requestId, - foodManufacturerId: manufacturerId, - status: OrderStatus.PENDING, - assigneeId: userId, - }); + const order = orderTransactionRepo.create({ + requestId: requestId, + foodManufacturerId: manufacturerId, + status: OrderStatus.PENDING, + assigneeId: userId, + }); - const savedOrder = await orderTransactionRepo.save(order); + const savedOrder = await orderTransactionRepo.save(order); - await this.allocationsService.createMultiple( - savedOrder.orderId, - itemAllocations, - transactionManager, - ); + await this.allocationsService.createMultiple( + savedOrder.orderId, + itemAllocations, + transactionManager, + ); - const associatedDonationIdsSet = - await this.donationItemsService.getAssociatedDonationIds( - donationItemIds, + const associatedDonationIdsSet = + await this.donationItemsService.getAssociatedDonationIds( + donationItemIds, + ); + + await this.donationService.matchAll( + Array.from(associatedDonationIdsSet), + transactionManager, ); - await this.donationService.matchAll( - Array.from(associatedDonationIdsSet), - transactionManager, - ); + const assignee = await this.usersService.findOne(userId); - const assignee = await this.usersService.findOne(userId); + return { + savedOrder, + request, + manufacturer, + assignee, + itemDetails, + }; + }); - try { - const pantryMessage = emailTemplates.pantryRequestMatchedOrder({ - pantryName: request.pantry.pantryName, - items: itemDetails, - brand: manufacturer.foodManufacturerName, - volunteerName: assignee.firstName + ' ' + assignee.lastName, - volunteerEmail: assignee.email, - }); - 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', - ); - } + const emailErrors: string[] = []; - 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: assignee.firstName + ' ' + assignee.lastName, - volunteerEmail: assignee.email, - }); - 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', - ); - } + try { + const pantryMessage = emailTemplates.pantryRequestMatchedOrder({ + pantryName: request.pantry.pantryName, + items: itemDetails, + brand: manufacturer.foodManufacturerName, + volunteerName: assignee.firstName + ' ' + assignee.lastName, + volunteerEmail: assignee.email, + }); + await this.emailsService.sendEmails( + [request.pantry.pantryUser.email], + pantryMessage.subject, + pantryMessage.bodyHTML, + ); + } catch { + emailErrors.push( + 'Failed to send pantry request matched order confirmation email', + ); + } - return savedOrder; - }); + 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: assignee.firstName + ' ' + assignee.lastName, + volunteerEmail: assignee.email, + }); + await this.emailsService.sendEmails( + [manufacturer.foodManufacturerRepresentative.email], + fmMessage.subject, + fmMessage.bodyHTML, + ); + } catch { + emailErrors.push( + 'Failed to send food manufacturer donation matched order confirmation email', + ); + } + + if (emailErrors.length > 0) { + this.logger.warn( + `Order ${ + savedOrder.orderId + } created, but email issues occurred: ${emailErrors.join('; ')}`, + ); + } + + return savedOrder; } async findOne(orderId: number): Promise { From 2470ddbe8bcea8eeac1698369ae4bf9dce3482f9 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Mon, 4 May 2026 15:44:11 -0400 Subject: [PATCH 07/20] comments --- apps/backend/src/emails/emailTemplates.ts | 20 +++++++++----------- apps/backend/src/orders/order.service.ts | 19 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts index 0afe30eed..01916ec11 100644 --- a/apps/backend/src/emails/emailTemplates.ts +++ b/apps/backend/src/emails/emailTemplates.ts @@ -4,7 +4,7 @@ export type EmailTemplate = { additionalContent?: string; }; -export const EMAIL_REDIRECT_URL = 'localhost:4200'; +export const EMAIL_REDIRECT_URL = 'https://localhost:4200'; // TODO: Change this before production to be the actual ssf email export const SSF_PARTNER_EMAIL = 'example@gmail.com'; @@ -121,20 +121,19 @@ export const emailTemplates = {

- To view full order details, delivery updates, and any notes from the coordinating volunteer or food manufacturer, please log into the platform. + 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}. + } 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 -

`, }), @@ -165,7 +164,7 @@ export const emailTemplates = { ${params.pantryAddress}

- Please log into the platform to review the full delivery details, timelines, and any special handling instructions associated with this shipment. + 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. @@ -173,15 +172,14 @@ export const emailTemplates = {

If you have any questions or need assistance, please contact your coordinator, ${ params.volunteerName - } at ${params.volunteerEmail}. + } 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 -

`, }), }; diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 2a3011129..2db2ae5e7 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -335,20 +335,19 @@ export class OrdersService { } try { + const pantryAddress = [ + request.pantry.mailingAddressLine1, + `${request.pantry.mailingAddressCity}, ${request.pantry.mailingAddressState} ${request.pantry.mailingAddressZip}`, + `[${request.pantry.mailingAddressCountry}]`, + ] + .filter(Boolean) + .join('
'); + 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, + pantryAddress: pantryAddress, volunteerName: assignee.firstName + ' ' + assignee.lastName, volunteerEmail: assignee.email, }); From aa7871a4322af62eca2daad1925df80f4864ff10 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Mon, 4 May 2026 21:29:29 -0400 Subject: [PATCH 08/20] finish test --- apps/backend/src/orders/order.service.spec.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 312dc89d5..50bd8cf0e 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -903,13 +903,11 @@ describe('OrdersService', () => { const pantry = request!.pantry; const pantryAddress = [ pantry.mailingAddressLine1, - pantry.mailingAddressCity, - pantry.mailingAddressState, - pantry.mailingAddressZip, - pantry.mailingAddressCountry, + `${pantry.mailingAddressCity}, ${pantry.mailingAddressState} ${pantry.mailingAddressZip}`, + `[${pantry.mailingAddressCountry}]`, ] - .join(' ') - .replace(/, ,/g, ', '); + .filter(Boolean) + .join('
'); const itemDetails = [ { quantity: '10', product: updatedDonationItem1!.itemName }, From 0c8f55a0d3ecb065f36bec12cc2ba8a029454b65 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 5 May 2026 11:15:56 -0400 Subject: [PATCH 09/20] tests and comments --- apps/backend/src/emails/emailTemplates.ts | 8 +- apps/backend/src/orders/order.service.spec.ts | 272 ++++++++++++------ apps/backend/src/orders/order.service.ts | 7 +- 3 files changed, 193 insertions(+), 94 deletions(-) diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts index 01916ec11..15f782d6f 100644 --- a/apps/backend/src/emails/emailTemplates.ts +++ b/apps/backend/src/emails/emailTemplates.ts @@ -112,14 +112,12 @@ export const emailTemplates = {

Good news! Your recent food request through Securing Safe Food has been successfully matched to an order and is now moving forward toward delivery.

-

- Items you will receive 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.

@@ -152,13 +150,13 @@ 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 Items:
+

Matched Items:

    ${params.items .map((item) => `
  • ${item.quantity} of ${item.product}
  • `) .join('')}
+

Recipient Pantry: ${params.pantryName}
Address:
${params.pantryAddress} diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 50bd8cf0e..4fa4cdaa4 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -26,7 +26,7 @@ import { AuthService } from '../auth/auth.service'; import { DonationService } from '../donations/donations.service'; import { PantriesService } from '../pantries/pantries.service'; import { CreateOrderDto } from './dtos/create-order.dto'; -import { DataSource } from 'typeorm'; +import { DataSource, EntityManager } from 'typeorm'; import { EmailsService } from '../emails/email.service'; import { Allocation } from '../allocations/allocations.entity'; import { mock } from 'jest-mock-extended'; @@ -796,6 +796,10 @@ describe('OrdersService', () => { ]); }); + afterEach(() => { + jest.restoreAllMocks(); + }); + it('should create a new order successfully and send appropriate emails', async () => { const allocationRepo = testDataSource.getRepository(Allocation); const donationItemRepo = testDataSource.getRepository(DonationItem); @@ -890,17 +894,19 @@ describe('OrdersService', () => { // Testing emails section - const assignee = await usersRepo.findOne({ where: { id: userId } }); - const request = await requestRepo.findOne({ + const assignee = (await usersRepo.findOne({ + where: { id: userId }, + })) as User; + const request = (await requestRepo.findOne({ where: { requestId: validCreateOrderDto.foodRequestId }, relations: ['pantry', 'pantry.pantryUser'], - }); - const manufacturer = await manufacturerRepo.findOne({ + })) as FoodRequest; + const manufacturer = (await manufacturerRepo.findOne({ where: { foodManufacturerId: validCreateOrderDto.manufacturerId }, relations: ['foodManufacturerRepresentative'], - }); + })) as FoodManufacturer; - const pantry = request!.pantry; + const pantry = request.pantry; const pantryAddress = [ pantry.mailingAddressLine1, `${pantry.mailingAddressCity}, ${pantry.mailingAddressState} ${pantry.mailingAddressZip}`, @@ -910,42 +916,40 @@ describe('OrdersService', () => { .join('
'); const itemDetails = [ - { quantity: '10', product: updatedDonationItem1!.itemName }, - { quantity: '3', product: updatedDonationItem2!.itemName }, - { quantity: '5', product: updatedDonationItem3!.itemName }, + { 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, - }); + const fmMessage = emailTemplates.fmDonationMatchedOrder({ + manufacturerName: manufacturer.foodManufacturerName, + items: itemDetails, + pantryName: pantry.pantryName, + pantryAddress, + volunteerName: assignee.firstName + ' ' + assignee.lastName, + volunteerEmail: assignee.email, + }); + + const pantryMessage = 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, + [request.pantry.pantryUser.email], + pantryMessage.subject, + pantryMessage.bodyHTML, ); expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( - [manufacturer!.foodManufacturerRepresentative.email], - fmSubject, - fmBodyHtml, + [manufacturer.foodManufacturerRepresentative.email], + fmMessage.subject, + fmMessage.bodyHTML, ); }); @@ -968,17 +972,10 @@ describe('OrdersService', () => { parsedAllocations, userId, ), - ).rejects.toThrow(BadRequestException); - await expect( - service.create( - validCreateOrderDto.foodRequestId, - validCreateOrderDto.manufacturerId, - parsedAllocations, - userId, - ), - ).rejects.toThrow( - `Request ${validCreateOrderDto.foodRequestId} is not active`, - ); + ).rejects.toMatchObject({ + name: BadRequestException.name, + message: `Request ${validCreateOrderDto.foodRequestId} is not active`, + }); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -1000,17 +997,10 @@ describe('OrdersService', () => { parsedAllocations, userId, ), - ).rejects.toThrow(BadRequestException); - await expect( - service.create( - validCreateOrderDto.foodRequestId, - validCreateOrderDto.manufacturerId, - parsedAllocations, - userId, - ), - ).rejects.toThrow( - `Manufacturer ${validCreateOrderDto.manufacturerId} is not approved`, - ); + ).rejects.toMatchObject({ + name: BadRequestException.name, + message: `Manufacturer ${validCreateOrderDto.manufacturerId} is not approved`, + }); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -1031,15 +1021,10 @@ describe('OrdersService', () => { parsedAllocations, userId, ), - ).rejects.toThrow(NotFoundException); - await expect( - service.create( - validCreateOrderDto.foodRequestId, - validCreateOrderDto.manufacturerId, - parsedAllocations, - userId, - ), - ).rejects.toThrow(`Donation items not found for ID(s): 999`); + ).rejects.toMatchObject({ + name: NotFoundException.name, + message: 'Donation items not found for ID(s): 999', + }); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -1064,17 +1049,10 @@ describe('OrdersService', () => { parsedAllocations, userId, ), - ).rejects.toThrow(BadRequestException); - await expect( - service.create( - validCreateOrderDto.foodRequestId, - validCreateOrderDto.manufacturerId, - parsedAllocations, - userId, - ), - ).rejects.toThrow( - `Donation item ${donationItemId} quantity to allocate exceeds remaining quantity`, - ); + ).rejects.toMatchObject({ + name: BadRequestException.name, + message: `Donation item ${donationItemId} quantity to allocate exceeds remaining quantity`, + }); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -1098,7 +1076,131 @@ describe('OrdersService', () => { parsedAllocations, userId, ), - ).rejects.toThrow(BadRequestException); + ).rejects.toMatchObject({ + name: BadRequestException.name, + message: `The following donation items are not associated with the current food manufacturer: Donation item ID ${donationItemId} with Donation ID 3`, + }); + + // Asserting that donation item reserved quantity wasn't updated + const donationItem1 = await donationItemRepo.findOne({ + where: { itemId: 1 }, + }); + expect(donationItem1?.reservedQuantity).toBe(10); + }); + + it('should still create order and send FM email when pantry email fails', async () => { + mockEmailsService.sendEmails.mockRejectedValueOnce( + new Error('SMTP error'), + ); + + const createdOrder = await service.create( + validCreateOrderDto.foodRequestId, + validCreateOrderDto.manufacturerId, + parsedAllocations, + userId, + ); + + expect(createdOrder).toBeDefined(); + expect(createdOrder.orderId).toBeDefined(); + expect(createdOrder.status).toEqual(OrderStatus.PENDING); + + expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2); + + const manufacturerRepo = testDataSource.getRepository(FoodManufacturer); + const manufacturer = await manufacturerRepo.findOne({ + where: { foodManufacturerId: validCreateOrderDto.manufacturerId }, + relations: ['foodManufacturerRepresentative'], + }); + expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( + [manufacturer!.foodManufacturerRepresentative.email], + expect.any(String), + expect.any(String), + ); + }); + + it('should still create order when both emails fail', async () => { + mockEmailsService.sendEmails.mockRejectedValueOnce( + new Error('SMTP error'), + ); + mockEmailsService.sendEmails.mockRejectedValueOnce( + new Error('SMTP error'), + ); + + const createdOrder = await service.create( + validCreateOrderDto.foodRequestId, + validCreateOrderDto.manufacturerId, + parsedAllocations, + userId, + ); + + expect(createdOrder).toBeDefined(); + expect(createdOrder.orderId).toBeDefined(); + expect(createdOrder.status).toEqual(OrderStatus.PENDING); + + expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2); + }); + + it('should call allocationsService.createMultiple once with correct parameters', async () => { + const spy = jest.spyOn( + (service as any).allocationsService as AllocationsService, + 'createMultiple', + ); + + const createdOrder = await service.create( + validCreateOrderDto.foodRequestId, + validCreateOrderDto.manufacturerId, + parsedAllocations, + userId, + ); + + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + createdOrder.orderId, + parsedAllocations, + expect.anything(), + ); + }); + + it('should call donationService.matchAll once with correct parameters', async () => { + const spy = jest.spyOn( + (service as any).donationService as DonationService, + 'matchAll', + ); + + await service.create( + validCreateOrderDto.foodRequestId, + validCreateOrderDto.manufacturerId, + parsedAllocations, + userId, + ); + + // Items 1 and 2 both belong to donation_id 1 + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + expect.arrayContaining([1]), + expect.any(EntityManager), + ); + }); + + it('should rollback transaction and not create order if allocation creation fails', async () => { + const orderRepo = testDataSource.getRepository(Order); + const donationItemRepo = testDataSource.getRepository(DonationItem); + + const orderCountBefore = await orderRepo.count(); + const item1Before = await donationItemRepo.findOne({ + where: { itemId: 1 }, + }); + const item2Before = await donationItemRepo.findOne({ + where: { itemId: 2 }, + }); + + jest + .spyOn( + (service as any).allocationsService as AllocationsService, + 'createMultiple', + ) + .mockRejectedValueOnce(new Error('DB error')); + await expect( service.create( validCreateOrderDto.foodRequestId, @@ -1106,15 +1208,19 @@ describe('OrdersService', () => { parsedAllocations, userId, ), - ).rejects.toThrow( - `The following donation items are not associated with the current food manufacturer: Donation item ID ${donationItemId} with Donation ID 3`, - ); + ).rejects.toThrow('DB error'); - // Asserting that donation item reserved quantity wasn't updated - const donationItem1 = await donationItemRepo.findOne({ + const orderCountAfter = await orderRepo.count(); + expect(orderCountAfter).toBe(orderCountBefore); + + const item1After = await donationItemRepo.findOne({ where: { itemId: 1 }, }); - expect(donationItem1?.reservedQuantity).toBe(10); + const item2After = await donationItemRepo.findOne({ + where: { itemId: 2 }, + }); + expect(item1After?.reservedQuantity).toBe(item1Before?.reservedQuantity); + expect(item2After?.reservedQuantity).toBe(item2Before?.reservedQuantity); }); }); describe('getAllOrdersForVolunteer', () => { diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 2db2ae5e7..e42ac9cee 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -292,13 +292,8 @@ export class OrdersService { transactionManager, ); - const associatedDonationIdsSet = - await this.donationItemsService.getAssociatedDonationIds( - donationItemIds, - ); - await this.donationService.matchAll( - Array.from(associatedDonationIdsSet), + [...new Set(donationItems.map((item) => item.donationId))], transactionManager, ); From 8446636213c84fce448a67e228175d4004a08a41 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Wed, 6 May 2026 10:35:52 -0400 Subject: [PATCH 10/20] shipment address instead of mailing --- apps/backend/src/orders/order.service.spec.ts | 15 ++++++++------- apps/backend/src/orders/order.service.ts | 15 ++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 4fa4cdaa4..b8e5b594b 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -907,13 +907,14 @@ describe('OrdersService', () => { })) as FoodManufacturer; const pantry = request.pantry; - const pantryAddress = [ - pantry.mailingAddressLine1, - `${pantry.mailingAddressCity}, ${pantry.mailingAddressState} ${pantry.mailingAddressZip}`, - `[${pantry.mailingAddressCountry}]`, - ] - .filter(Boolean) - .join('
'); + const pantryAddress = `${pantry.shipmentAddressLine1}
+${pantry.shipmentAddressCity}, ${pantry.shipmentAddressState} ${ + pantry.shipmentAddressZip + }${ + pantry.shipmentAddressCountry + ? `
[${pantry.shipmentAddressCountry}]` + : '' + }`; const itemDetails = [ { quantity: '10', product: updatedDonationItem1.itemName }, diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index e42ac9cee..259448877 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -330,13 +330,14 @@ export class OrdersService { } try { - const pantryAddress = [ - request.pantry.mailingAddressLine1, - `${request.pantry.mailingAddressCity}, ${request.pantry.mailingAddressState} ${request.pantry.mailingAddressZip}`, - `[${request.pantry.mailingAddressCountry}]`, - ] - .filter(Boolean) - .join('
'); + const pantryAddress = `${request.pantry.shipmentAddressLine1}
+${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ + request.pantry.shipmentAddressZip + }${ + request.pantry.shipmentAddressCountry + ? `
[${request.pantry.shipmentAddressCountry}]` + : '' + }`; const fmMessage = emailTemplates.fmDonationMatchedOrder({ manufacturerName: manufacturer.foodManufacturerName, From 1b7750f79d8242f99c978cdb7e756b3220b71bdb Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Wed, 6 May 2026 16:57:50 -0400 Subject: [PATCH 11/20] food request closed email --- apps/backend/src/emails/emailTemplates.ts | 27 ++++ .../foodRequests/request.controller.spec.ts | 13 +- .../src/foodRequests/request.controller.ts | 5 +- .../src/foodRequests/request.module.ts | 4 + .../src/foodRequests/request.service.spec.ts | 132 +++++++++++++++++- .../src/foodRequests/request.service.ts | 67 ++++++++- 6 files changed, 235 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts index 15f782d6f..63ab61462 100644 --- a/apps/backend/src/emails/emailTemplates.ts +++ b/apps/backend/src/emails/emailTemplates.ts @@ -135,6 +135,33 @@ export const emailTemplates = { `, }), + pantryRequestClosed: (params: { + pantryName: string; + volunteerName: string; + volunteerEmail: string; + }): EmailTemplate => ({ + subject: 'Your Securing Safe Food Request Has Been Completed', + bodyHTML: ` +

Hi ${params.pantryName},

+

+ Your recent food request through Securing Safe Food has been marked as complete. + We are glad to fulfill your pantry's requests! If you would like to continue receiving + donations, please submit a new food request at any time to ensure there is no interruption + in future deliveries. +

+

+ To submit a new request or view past orders, please log into the platform here: + ${EMAIL_REDIRECT_URL}/login +

+

+ If you have any questions or feedback about this order, please do not hesitate to reach out. + You can contact your pantry coordinator, ${params.volunteerName}, at + ${params.volunteerEmail}. +

+

Best regards,
The Securing Safe Food Team

+ `, + }), + fmDonationMatchedOrder: (params: { manufacturerName: string; items: { quantity: string; product: string }[]; diff --git a/apps/backend/src/foodRequests/request.controller.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index dfd76b647..ab7c8ae4f 100644 --- a/apps/backend/src/foodRequests/request.controller.spec.ts +++ b/apps/backend/src/foodRequests/request.controller.spec.ts @@ -16,6 +16,7 @@ import { } from './dtos/matching.dto'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { Pantry } from '../pantries/pantries.entity'; +import { AuthenticatedRequest } from '../auth/authenticated-request'; const mockRequestsService = mock(); @@ -309,10 +310,18 @@ describe('RequestsController', () => { closedRequest as FoodRequest, ); - const result = await controller.closeRequest(requestId); + const req = { user: { id: 1 } }; + + const result = await controller.closeRequest( + requestId, + req as AuthenticatedRequest, + ); expect(result).toEqual(closedRequest); - expect(mockRequestsService.closeRequest).toHaveBeenCalledWith(requestId); + expect(mockRequestsService.closeRequest).toHaveBeenCalledWith( + requestId, + 1, + ); }); }); }); diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index 9505d0aaf..9bdea232b 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -8,7 +8,9 @@ import { ValidationPipe, Patch, Delete, + Req, } from '@nestjs/common'; +import { AuthenticatedRequest } from '../auth/authenticated-request'; import { ApiBody } from '@nestjs/swagger'; import { RequestsService } from './request.service'; import { FoodRequest } from './request.entity'; @@ -124,7 +126,8 @@ export class RequestsController { @Patch('/:requestId/close') async closeRequest( @Param('requestId', ParseIntPipe) requestId: number, + @Req() req: AuthenticatedRequest, ): Promise { - return this.requestsService.closeRequest(requestId); + return this.requestsService.closeRequest(requestId, req.user.id); } } diff --git a/apps/backend/src/foodRequests/request.module.ts b/apps/backend/src/foodRequests/request.module.ts index dcec30601..3b6535c2e 100644 --- a/apps/backend/src/foodRequests/request.module.ts +++ b/apps/backend/src/foodRequests/request.module.ts @@ -9,6 +9,8 @@ import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { DonationItem } from '../donationItems/donationItems.entity'; import { EmailsModule } from '../emails/email.module'; +import { User } from '../users/users.entity'; +import { UsersModule } from '../users/users.module'; @Module({ imports: [ @@ -18,9 +20,11 @@ import { EmailsModule } from '../emails/email.module'; Pantry, FoodManufacturer, DonationItem, + User, ]), AuthModule, EmailsModule, + UsersModule, ], 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 13be82b49..d69d214f6 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -20,6 +20,12 @@ import { mock } from 'jest-mock-extended'; import { emailTemplates } from '../emails/emailTemplates'; import { Allocation } from '../allocations/allocations.entity'; import { ApplicationStatus } from '../shared/types'; +import { User } from '../users/users.entity'; +import { UsersService } from '../users/users.service'; +import { Donation } from '../donations/donations.entity'; +import { AuthService } from '../auth/auth.service'; +import { PantriesService } from '../pantries/pantries.service'; +import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; jest.setTimeout(60000); @@ -38,6 +44,9 @@ describe('RequestsService', () => { const module = await Test.createTestingModule({ providers: [ RequestsService, + UsersService, + PantriesService, + FoodManufacturersService, { provide: getRepositoryToken(FoodRequest), useValue: testDataSource.getRepository(FoodRequest), @@ -62,6 +71,20 @@ describe('RequestsService', () => { provide: getRepositoryToken(Allocation), useValue: testDataSource.getRepository(Allocation), }, + { + provide: getRepositoryToken(User), + useValue: testDataSource.getRepository(User), + }, + { + provide: getRepositoryToken(Donation), + useValue: testDataSource.getRepository(Donation), + }, + { + provide: AuthService, + useValue: { + adminCreateUser: jest.fn().mockResolvedValue('test-sub'), + }, + }, { provide: EmailsService, useValue: mockEmailsService, @@ -381,6 +404,64 @@ describe('RequestsService', () => { new NotFoundException('Request 999 not found'), ); }); + + it('sends pantry closed email with last delivered order assignee on auto-close', async () => { + const requestId = 1; + const pantry = await testDataSource.getRepository(Pantry).findOne({ + where: { pantryId: 1 }, + relations: ['pantryUser'], + }); + const lastDeliveredOrder = await testDataSource + .getRepository(Order) + .findOne({ + where: { requestId, status: OrderStatus.DELIVERED }, + order: { deliveredAt: 'DESC' }, + relations: ['assignee'], + }); + + await service.updateRequestStatus(requestId); + + const assignee = lastDeliveredOrder!.assignee; + const expectedMessage = emailTemplates.pantryRequestClosed({ + pantryName: pantry!.pantryName, + volunteerName: `${assignee.firstName} ${assignee.lastName}`, + volunteerEmail: assignee.email, + }); + + expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); + expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( + [pantry!.pantryUser.email], + expectedMessage.subject, + expectedMessage.bodyHTML, + ); + }); + + it('does not send email when not all orders are delivered (request stays active)', async () => { + await service.updateRequestStatus(3); + + expect(mockEmailsService.sendEmails).not.toHaveBeenCalled(); + }); + + it('does not send email when request was already closed before updateRequestStatus', async () => { + await testDataSource.query( + `UPDATE food_requests SET status = 'closed' WHERE request_id = 1`, + ); + + await service.updateRequestStatus(1); + + expect(mockEmailsService.sendEmails).not.toHaveBeenCalled(); + }); + + it('still auto-closes request when email fails', async () => { + mockEmailsService.sendEmails.mockRejectedValueOnce( + new Error('SMTP error'), + ); + + await service.updateRequestStatus(1); + + const request = await service.findOne(1); + expect(request.status).toBe(FoodRequestStatus.CLOSED); + }); }); describe('getMatchingManufacturers', () => { @@ -746,8 +827,14 @@ describe('RequestsService', () => { }); describe('closeRequest', () => { + let volunteerId: number; + + beforeEach(() => { + volunteerId = 6; + }); + it('should close an active request', async () => { - const result = await service.closeRequest(3); + const result = await service.closeRequest(3, volunteerId); expect(result.status).toBe(FoodRequestStatus.CLOSED); @@ -756,15 +843,15 @@ describe('RequestsService', () => { }); it('should throw BadRequestException when request is already closed', async () => { - await service.closeRequest(3); + await service.closeRequest(3, volunteerId); - await expect(service.closeRequest(3)).rejects.toThrow( + await expect(service.closeRequest(3, volunteerId)).rejects.toThrow( new BadRequestException('Cannot close a request with status: closed'), ); }); it('should throw NotFoundException for non-existent request', async () => { - await expect(service.closeRequest(999)).rejects.toThrow( + await expect(service.closeRequest(999, volunteerId)).rejects.toThrow( new NotFoundException('Request 999 not found'), ); }); @@ -774,7 +861,7 @@ describe('RequestsService', () => { .getRepository(Order) .find({ where: { requestId: 3 } }); - await service.closeRequest(3); + await service.closeRequest(3, volunteerId); const ordersAfter = await testDataSource .getRepository(Order) @@ -786,11 +873,44 @@ describe('RequestsService', () => { }); it('should not reopen a closed request when updateRequestStatus is called', async () => { - await service.closeRequest(1); + await service.closeRequest(1, volunteerId); await service.updateRequestStatus(1); const fromDb = await service.findOne(1); expect(fromDb.status).toBe(FoodRequestStatus.CLOSED); }); + + it('sends pantry closed email with acting volunteer info on successful close', async () => { + const pantry = await testDataSource.getRepository(Pantry).findOne({ + where: { pantryId: 3 }, + relations: ['pantryUser'], + }); + + await service.closeRequest(3, volunteerId); + + const expectedMessage = emailTemplates.pantryRequestClosed({ + pantryName: pantry!.pantryName, + volunteerName: `James Thomas`, + volunteerEmail: `james.t@volunteer.org`, + }); + + expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); + expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( + [pantry!.pantryUser.email], + expectedMessage.subject, + expectedMessage.bodyHTML, + ); + }); + + it('still closes request when email fails (manual close)', async () => { + mockEmailsService.sendEmails.mockRejectedValueOnce( + new Error('SMTP error'), + ); + + const result = await service.closeRequest(3, volunteerId); + + expect(result.status).toBe(FoodRequestStatus.CLOSED); + expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index ee7d771e7..286ef4e11 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -2,6 +2,7 @@ import { BadRequestException, Injectable, InternalServerErrorException, + Logger, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -24,9 +25,12 @@ import { DonationItem } from '../donationItems/donationItems.entity'; import { EmailsService } from '../emails/email.service'; import { emailTemplates } from '../emails/emailTemplates'; import { UpdateRequestDto } from './dtos/update-request.dto'; +import { UsersService } from '../users/users.service'; @Injectable() export class RequestsService { + private readonly logger = new Logger(RequestsService.name); + constructor( @InjectRepository(FoodRequest) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, @@ -36,6 +40,7 @@ export class RequestsService { @InjectRepository(DonationItem) private donationItemRepo: Repository, private emailsService: EmailsService, + private usersService: UsersService, ) {} async findOne(requestId: number): Promise { @@ -279,7 +284,7 @@ export class RequestsService { const request = await this.repo.findOne({ where: { requestId }, - relations: ['orders'], + relations: ['orders', 'pantry', 'pantry.pantryUser'], }); if (!request) { @@ -298,13 +303,43 @@ export class RequestsService { (order) => order.status === OrderStatus.DELIVERED, ); - if (request.status !== FoodRequestStatus.CLOSED) { + const wasAlreadyClosed = request.status === FoodRequestStatus.CLOSED; + + if (!wasAlreadyClosed) { request.status = allDelivered ? FoodRequestStatus.CLOSED : FoodRequestStatus.ACTIVE; } await this.repo.save(request); + + if (allDelivered && !wasAlreadyClosed) { + try { + const lastDeliveredOrder = await this.orderRepo.findOne({ + where: { requestId, status: OrderStatus.DELIVERED }, + order: { deliveredAt: 'DESC' }, + relations: ['assignee'], + }); + + if (lastDeliveredOrder?.assignee) { + const { assignee } = lastDeliveredOrder; + const message = emailTemplates.pantryRequestClosed({ + pantryName: request.pantry.pantryName, + volunteerName: `${assignee.firstName} ${assignee.lastName}`, + volunteerEmail: assignee.email, + }); + await this.emailsService.sendEmails( + [request.pantry.pantryUser.email], + message.subject, + message.bodyHTML, + ); + } + } catch { + this.logger.warn( + `Request ${requestId} auto-closed, but failed to send pantry notification email`, + ); + } + } } async update(requestId: number, dto: UpdateRequestDto): Promise { @@ -373,11 +408,15 @@ export class RequestsService { await this.repo.remove(request); } - async closeRequest(requestId: number): Promise { + async closeRequest( + requestId: number, + actingUserId: number, + ): Promise { validateId(requestId, 'Request'); const request = await this.repo.findOne({ where: { requestId }, + relations: ['pantry', 'pantry.pantryUser'], }); if (!request) { @@ -390,7 +429,27 @@ export class RequestsService { ); } + const assignee = await this.usersService.findOne(actingUserId); + request.status = FoodRequestStatus.CLOSED; - return this.repo.save(request); + const saved = await this.repo.save(request); + try { + const message = emailTemplates.pantryRequestClosed({ + pantryName: request.pantry.pantryName, + volunteerName: `${assignee.firstName} ${assignee.lastName}`, + volunteerEmail: assignee.email, + }); + await this.emailsService.sendEmails( + [request.pantry.pantryUser.email], + message.subject, + message.bodyHTML, + ); + } catch { + this.logger.warn( + `Request ${requestId} closed, but failed to send pantry notification email`, + ); + } + + return saved; } } From f658e67f8a40d847ef678c3061db8c28558d33b5 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Thu, 7 May 2026 16:38:36 -0400 Subject: [PATCH 12/20] comments --- apps/backend/src/emails/emailTemplates.ts | 4 +- .../src/foodRequests/request.service.spec.ts | 10 +++- .../src/foodRequests/request.service.ts | 4 +- apps/backend/src/orders/order.service.spec.ts | 59 ++++++++++++------- apps/backend/src/orders/order.service.ts | 18 +++--- 5 files changed, 59 insertions(+), 36 deletions(-) diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts index 63ab61462..a19024d63 100644 --- a/apps/backend/src/emails/emailTemplates.ts +++ b/apps/backend/src/emails/emailTemplates.ts @@ -150,7 +150,7 @@ export const emailTemplates = { in future deliveries.

- To submit a new request or view past orders, please log into the platform here: + To submit a new request or view past orders, please log into the platform here: ${EMAIL_REDIRECT_URL}/login

@@ -177,7 +177,7 @@ 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 Items:

+

Matched Items:

    ${params.items .map((item) => `
  • ${item.quantity} of ${item.product}
  • `) diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index d69d214f6..c88a57e3e 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -907,9 +907,15 @@ describe('RequestsService', () => { new Error('SMTP error'), ); - const result = await service.closeRequest(3, volunteerId); + await expect(service.closeRequest(3, volunteerId)).rejects.toThrow( + new InternalServerErrorException( + 'Failed to send food request closed email to pantry', + ), + ); - expect(result.status).toBe(FoodRequestStatus.CLOSED); + const request = await service.findOne(3); + + expect(request.status).toBe(FoodRequestStatus.CLOSED); expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); }); }); diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 286ef4e11..075ac77ec 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -445,8 +445,8 @@ export class RequestsService { message.bodyHTML, ); } catch { - this.logger.warn( - `Request ${requestId} closed, but failed to send pantry notification email`, + throw new InternalServerErrorException( + 'Failed to send food request closed email to pantry', ); } diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index b8e5b594b..de8d1fd49 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -6,7 +6,11 @@ import { testDataSource } from '../config/typeormTestDataSource'; import { OrderStatus, VolunteerAction } from './types'; import { Pantry } from '../pantries/pantries.entity'; import { OrderDetailsDto } from './dtos/order-details.dto'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; import { TrackingCostDto } from './dtos/tracking-cost.dto'; import { FoodType } from '../donationItems/types'; import { FoodRequest } from '../foodRequests/request.entity'; @@ -907,12 +911,16 @@ describe('OrdersService', () => { })) as FoodManufacturer; const pantry = request.pantry; - const pantryAddress = `${pantry.shipmentAddressLine1}
    -${pantry.shipmentAddressCity}, ${pantry.shipmentAddressState} ${ - pantry.shipmentAddressZip + const pantryAddress = `${request.pantry.shipmentAddressLine1}${ + request.pantry.shipmentAddressLine2 + ? `
    ${request.pantry.shipmentAddressLine2}` + : '' + }
    +${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ + request.pantry.shipmentAddressZip }${ - pantry.shipmentAddressCountry - ? `
    [${pantry.shipmentAddressCountry}]` + request.pantry.shipmentAddressCountry + ? `
    ${request.pantry.shipmentAddressCountry}` : '' }`; @@ -1094,15 +1102,21 @@ ${pantry.shipmentAddressCity}, ${pantry.shipmentAddressState} ${ new Error('SMTP error'), ); - const createdOrder = await service.create( - validCreateOrderDto.foodRequestId, - validCreateOrderDto.manufacturerId, - parsedAllocations, - userId, + await expect( + service.create( + validCreateOrderDto.foodRequestId, + validCreateOrderDto.manufacturerId, + parsedAllocations, + userId, + ), + ).rejects.toThrow( + new InternalServerErrorException( + 'Failed to send pantry request matched order confirmation email', + ), ); - expect(createdOrder).toBeDefined(); - expect(createdOrder.orderId).toBeDefined(); + const createdOrder = await service.findOne(5); + expect(createdOrder.status).toEqual(OrderStatus.PENDING); expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2); @@ -1127,15 +1141,20 @@ ${pantry.shipmentAddressCity}, ${pantry.shipmentAddressState} ${ new Error('SMTP error'), ); - const createdOrder = await service.create( - validCreateOrderDto.foodRequestId, - validCreateOrderDto.manufacturerId, - parsedAllocations, - userId, + await expect( + service.create( + validCreateOrderDto.foodRequestId, + validCreateOrderDto.manufacturerId, + parsedAllocations, + userId, + ), + ).rejects.toThrow( + new InternalServerErrorException( + 'Failed to send pantry request matched order confirmation email; Failed to send food manufacturer donation matched order confirmation email', + ), ); - expect(createdOrder).toBeDefined(); - expect(createdOrder.orderId).toBeDefined(); + const createdOrder = await service.findOne(5); expect(createdOrder.status).toEqual(OrderStatus.PENDING); expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 259448877..0860b35c7 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Injectable, NotFoundException, - Logger, + InternalServerErrorException, } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { Repository, In, DataSource } from 'typeorm'; @@ -32,8 +32,6 @@ import { UsersService } from '../users/users.service'; @Injectable() export class OrdersService { - private readonly logger = new Logger(OrdersService.name); - constructor( @InjectRepository(Order) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, @@ -330,12 +328,16 @@ export class OrdersService { } try { - const pantryAddress = `${request.pantry.shipmentAddressLine1}
    + const pantryAddress = `${request.pantry.shipmentAddressLine1}${ + request.pantry.shipmentAddressLine2 + ? `
    ${request.pantry.shipmentAddressLine2}` + : '' + }
    ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ request.pantry.shipmentAddressZip }${ request.pantry.shipmentAddressCountry - ? `
    [${request.pantry.shipmentAddressCountry}]` + ? `
    ${request.pantry.shipmentAddressCountry}` : '' }`; @@ -359,11 +361,7 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ } if (emailErrors.length > 0) { - this.logger.warn( - `Order ${ - savedOrder.orderId - } created, but email issues occurred: ${emailErrors.join('; ')}`, - ); + throw new InternalServerErrorException(emailErrors.join('; ')); } return savedOrder; From 685bc01931217e31fc5d410566ae059c125294a9 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Wed, 13 May 2026 11:11:45 -0400 Subject: [PATCH 13/20] minor refactoring comments --- .../src/foodRequests/request.service.spec.ts | 47 +++++++++++++------ apps/backend/src/orders/order.service.spec.ts | 38 ++++++--------- apps/backend/src/orders/order.service.ts | 1 + 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index c88a57e3e..77b619a81 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -407,30 +407,36 @@ describe('RequestsService', () => { it('sends pantry closed email with last delivered order assignee on auto-close', async () => { const requestId = 1; - const pantry = await testDataSource.getRepository(Pantry).findOne({ + const pantry = (await testDataSource.getRepository(Pantry).findOne({ where: { pantryId: 1 }, relations: ['pantryUser'], - }); - const lastDeliveredOrder = await testDataSource + })) as Pantry; + const lastDeliveredOrder = (await testDataSource .getRepository(Order) .findOne({ where: { requestId, status: OrderStatus.DELIVERED }, order: { deliveredAt: 'DESC' }, relations: ['assignee'], - }); + })) as Order; + + const requestBefore = await service.findOne(1); + expect(requestBefore.status).toBe(FoodRequestStatus.ACTIVE); await service.updateRequestStatus(requestId); - const assignee = lastDeliveredOrder!.assignee; + const request = await service.findOne(1); + expect(request.status).toBe(FoodRequestStatus.CLOSED); + + const assignee = lastDeliveredOrder.assignee; const expectedMessage = emailTemplates.pantryRequestClosed({ - pantryName: pantry!.pantryName, + pantryName: pantry.pantryName, volunteerName: `${assignee.firstName} ${assignee.lastName}`, volunteerEmail: assignee.email, }); expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( - [pantry!.pantryUser.email], + [pantry.pantryUser.email], expectedMessage.subject, expectedMessage.bodyHTML, ); @@ -447,20 +453,31 @@ describe('RequestsService', () => { `UPDATE food_requests SET status = 'closed' WHERE request_id = 1`, ); + const request = await service.findOne(1); + expect(request.status).toBe(FoodRequestStatus.CLOSED); + await service.updateRequestStatus(1); expect(mockEmailsService.sendEmails).not.toHaveBeenCalled(); }); it('still auto-closes request when email fails', async () => { + const requestBefore = await service.findOne(1); + expect(requestBefore.status).toBe(FoodRequestStatus.ACTIVE); + mockEmailsService.sendEmails.mockRejectedValueOnce( new Error('SMTP error'), ); + const loggerSpy = jest.spyOn(service['logger'], 'warn'); + await service.updateRequestStatus(1); const request = await service.findOne(1); expect(request.status).toBe(FoodRequestStatus.CLOSED); + expect(loggerSpy).toHaveBeenCalledWith( + 'Request 1 auto-closed, but failed to send pantry notification email', + ); }); }); @@ -485,13 +502,13 @@ describe('RequestsService', () => { const manufacturerRepo = testDataSource.getRepository(FoodManufacturer); - const manufacturer = await manufacturerRepo.findOne({ + const manufacturer = (await manufacturerRepo.findOne({ where: { foodManufacturerId: 1 }, - }); + })) as FoodManufacturer; - manufacturer!.status = ApplicationStatus.PENDING; + manufacturer.status = ApplicationStatus.PENDING; - await manufacturerRepo.save(manufacturer!); + await manufacturerRepo.save(manufacturer); const resultAfter = await service.getMatchingManufacturers(requestId); @@ -881,22 +898,22 @@ describe('RequestsService', () => { }); it('sends pantry closed email with acting volunteer info on successful close', async () => { - const pantry = await testDataSource.getRepository(Pantry).findOne({ + const pantry = (await testDataSource.getRepository(Pantry).findOne({ where: { pantryId: 3 }, relations: ['pantryUser'], - }); + })) as Pantry; await service.closeRequest(3, volunteerId); const expectedMessage = emailTemplates.pantryRequestClosed({ - pantryName: pantry!.pantryName, + pantryName: pantry.pantryName, volunteerName: `James Thomas`, volunteerEmail: `james.t@volunteer.org`, }); expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( - [pantry!.pantryUser.email], + [pantry.pantryUser.email], expectedMessage.subject, expectedMessage.bodyHTML, ); diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index de8d1fd49..5109596e2 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -779,7 +779,7 @@ describe('OrdersService', () => { }); }); - describe('createOrder', () => { + describe('create', () => { let validCreateOrderDto: CreateOrderDto; let parsedAllocations: Map; const userId = 3; @@ -815,18 +815,15 @@ describe('OrdersService', () => { parsedAllocations.set(9, 5); // Initial donation items - const donationItem1 = await donationItemRepo.findOne({ + const donationItem1 = (await donationItemRepo.findOne({ where: { itemId: 1 }, - }); - const donationItem2 = await donationItemRepo.findOne({ + })) as DonationItem; + const donationItem2 = (await donationItemRepo.findOne({ where: { itemId: 2 }, - }); - const donationItem3 = await donationItemRepo.findOne({ + })) as DonationItem; + const donationItem3 = (await donationItemRepo.findOne({ where: { itemId: 9 }, - }); - - if (!donationItem1 || !donationItem2 || !donationItem3) - throw new Error('Missing dummy donation items'); + })) as DonationItem; donationItem3.quantity = 100; @@ -859,23 +856,16 @@ describe('OrdersService', () => { expect.arrayContaining([10, 3, 5]), ); - const updatedDonationItem1 = await donationItemRepo.findOne({ + const updatedDonationItem1 = (await donationItemRepo.findOne({ where: { itemId: 1 }, - }); - const updatedDonationItem2 = await donationItemRepo.findOne({ + })) as DonationItem; + const updatedDonationItem2 = (await donationItemRepo.findOne({ where: { itemId: 2 }, - }); - const updatedDonationItem3 = await donationItemRepo.findOne({ + })) as DonationItem; + const updatedDonationItem3 = (await donationItemRepo.findOne({ where: { itemId: 9 }, - }); + })) as DonationItem; - if ( - !updatedDonationItem1 || - !updatedDonationItem2 || - !updatedDonationItem3 - ) { - throw new Error('Missing donation item test object'); - } expect(updatedDonationItem1.reservedQuantity).toBe( donationItem1.reservedQuantity + 10, ); @@ -1177,7 +1167,7 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ expect(spy).toHaveBeenCalledWith( createdOrder.orderId, parsedAllocations, - expect.anything(), + expect.any(EntityManager), ); }); diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 0860b35c7..73ed435fb 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -200,6 +200,7 @@ export class OrdersService { await this.dataSource.transaction(async (transactionManager) => { validateId(manufacturerId, 'Food Manufacturer'); validateId(requestId, 'Request'); + validateId(userId, 'User'); const request = await this.requestRepo.findOne({ where: { requestId }, From d77f5b823b97f7c139647b00687aeeba9cc082c4 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Wed, 13 May 2026 13:24:43 -0400 Subject: [PATCH 14/20] some comments --- .../foodRequests/request.controller.spec.ts | 6 +- .../src/foodRequests/request.service.spec.ts | 7 ++ apps/backend/src/orders/order.service.spec.ts | 86 ++++++++++++++++++- apps/backend/src/orders/order.service.ts | 12 +++ 4 files changed, 107 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/foodRequests/request.controller.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index e8a3b918d..861f41d37 100644 --- a/apps/backend/src/foodRequests/request.controller.spec.ts +++ b/apps/backend/src/foodRequests/request.controller.spec.ts @@ -297,7 +297,9 @@ describe('RequestsController', () => { it('should call requestsService.closeRequest', async () => { const requestId = 1; - mockRequestsService.closeRequest.mockResolvedValueOnce(undefined); + mockRequestsService.closeRequest.mockResolvedValueOnce( + foodRequest1 as FoodRequest, + ); const req = { user: { id: 1 } }; @@ -306,7 +308,7 @@ describe('RequestsController', () => { req as AuthenticatedRequest, ); - expect(result).toEqual(closedRequest); + expect(result).toEqual(foodRequest1); expect(mockRequestsService.closeRequest).toHaveBeenCalledWith( requestId, 1, diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index c8ac94ab8..762b0a242 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -443,6 +443,13 @@ describe('RequestsService', () => { }); it('does not send email when not all orders are delivered (request stays active)', async () => { + const request = (await service.findOne(3)) as FoodRequest; + + expect(request.orders).toBeDefined(); + expect( + request.orders?.some((order) => order.status !== OrderStatus.DELIVERED), + ).toBe(true); + await service.updateRequestStatus(3); expect(mockEmailsService.sendEmails).not.toHaveBeenCalled(); diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 48f0d04a0..81717161f 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -11,8 +11,6 @@ import { InternalServerErrorException, NotFoundException, } from '@nestjs/common'; -import { TrackingCostDto } from './dtos/tracking-cost.dto'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; import { BulkUpdateTrackingCostDto } from './dtos/bulk-update-tracking-cost.dto'; import { FoodType } from '../donationItems/types'; import { FoodRequest } from '../foodRequests/request.entity'; @@ -1091,6 +1089,90 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ expect(item1After?.reservedQuantity).toBe(item1Before?.reservedQuantity); expect(item2After?.reservedQuantity).toBe(item2Before?.reservedQuantity); }); + + it('should rollback transaction and not create order if donation matching fails', async () => { + const orderRepo = testDataSource.getRepository(Order); + const donationItemRepo = testDataSource.getRepository(DonationItem); + + const orderCountBefore = await orderRepo.count(); + const item1Before = await donationItemRepo.findOne({ + where: { itemId: 1 }, + }); + const item2Before = await donationItemRepo.findOne({ + where: { itemId: 2 }, + }); + + jest + .spyOn((service as any).donationService as DonationService, 'matchAll') + .mockRejectedValueOnce(new Error('DB error')); + + await expect( + service.create( + validCreateOrderDto.foodRequestId, + validCreateOrderDto.manufacturerId, + parsedAllocations, + userId, + ), + ).rejects.toThrow('DB error'); + + const orderCountAfter = await orderRepo.count(); + expect(orderCountAfter).toBe(orderCountBefore); + + const item1After = await donationItemRepo.findOne({ + where: { itemId: 1 }, + }); + const item2After = await donationItemRepo.findOne({ + where: { itemId: 2 }, + }); + expect(item1After?.reservedQuantity).toBe(item1Before?.reservedQuantity); + expect(item2After?.reservedQuantity).toBe(item2Before?.reservedQuantity); + }); + + it('should throw BadRequestException if itemAllocations is empty', async () => { + const donationItemRepo = testDataSource.getRepository(DonationItem); + const emptyAllocations = new Map(); + + await expect( + service.create( + validCreateOrderDto.foodRequestId, + validCreateOrderDto.manufacturerId, + emptyAllocations, + userId, + ), + ).rejects.toMatchObject({ + name: BadRequestException.name, + message: 'Cannot create order with no donation items', + }); + + // Asserting that donation item reserved quantity wasn't updated + const donationItem1 = await donationItemRepo.findOne({ + where: { itemId: 1 }, + }); + expect(donationItem1?.reservedQuantity).toBe(10); + }); + + it('should throw NotFoundException if request is not found', async () => { + const nonExistentRequestId = 999; + const donationItemRepo = testDataSource.getRepository(DonationItem); + + await expect( + service.create( + nonExistentRequestId, + validCreateOrderDto.manufacturerId, + parsedAllocations, + userId, + ), + ).rejects.toMatchObject({ + name: NotFoundException.name, + message: `Request ${nonExistentRequestId} not found`, + }); + + // Asserting that donation item reserved quantity wasn't updated + const donationItem1 = await donationItemRepo.findOne({ + where: { itemId: 1 }, + }); + expect(donationItem1?.reservedQuantity).toBe(10); + }); }); describe('getAllOrdersForVolunteer', () => { it('should return all orders across all pantries and assignees, with required actions for assigned orders', async () => { diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 2309cb82b..3f898b759 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -230,6 +230,12 @@ export class OrdersService { select: ['donationId'], }); + if (fmDonations.length === 0) { + throw new BadRequestException( + `Manufacturer ${manufacturerId} has no donations`, + ); + } + const fmDonationIdSet = new Set(fmDonations.map((d) => d.donationId)); const donationItemIds = Array.from(itemAllocations.keys()); @@ -237,6 +243,12 @@ export class OrdersService { donationItemIds, ); + if (donationItems.length === 0) { + throw new BadRequestException( + 'Cannot create order with no donation items', + ); + } + const invalidItems = donationItems.filter( (item) => !fmDonationIdSet.has(item.donationId), ); From c78d2e816d10b8ac2a37da0f1d9d5cbadf2f723d Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 19 May 2026 10:30:19 -0400 Subject: [PATCH 15/20] comments --- apps/backend/src/foodRequests/request.service.spec.ts | 11 +++++------ apps/backend/src/foodRequests/request.service.ts | 5 +---- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index 762b0a242..068956341 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -476,15 +476,14 @@ describe('RequestsService', () => { new Error('SMTP error'), ); - const loggerSpy = jest.spyOn(service['logger'], 'warn'); - - await service.updateRequestStatus(1); + await expect(service.updateRequestStatus(1)).rejects.toThrow( + new InternalServerErrorException( + 'Request 1 auto-closed, but failed to send pantry notification email', + ), + ); const request = await service.findOne(1); expect(request.status).toBe(FoodRequestStatus.CLOSED); - expect(loggerSpy).toHaveBeenCalledWith( - 'Request 1 auto-closed, but failed to send pantry notification email', - ); }); }); diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index e4891a7fe..1fbb253d9 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, Injectable, InternalServerErrorException, - Logger, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -29,8 +28,6 @@ import { UsersService } from '../users/users.service'; @Injectable() export class RequestsService { - private readonly logger = new Logger(RequestsService.name); - constructor( @InjectRepository(FoodRequest) private repo: Repository, @InjectRepository(Pantry) private pantryRepo: Repository, @@ -336,7 +333,7 @@ export class RequestsService { ); } } catch { - this.logger.warn( + throw new InternalServerErrorException( `Request ${requestId} auto-closed, but failed to send pantry notification email`, ); } From 518e45b1c2f61f9fa865a5f203344dcecac86f97 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 26 May 2026 18:55:29 -0400 Subject: [PATCH 16/20] comments --- .../donationItems/donationItems.service.ts | 23 ------------------- apps/backend/src/emails/emailTemplates.ts | 6 +++-- .../src/foodRequests/request.service.ts | 6 ++++- apps/backend/src/orders/order.service.ts | 4 ++-- 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/apps/backend/src/donationItems/donationItems.service.ts b/apps/backend/src/donationItems/donationItems.service.ts index 168f46048..916210174 100644 --- a/apps/backend/src/donationItems/donationItems.service.ts +++ b/apps/backend/src/donationItems/donationItems.service.ts @@ -54,29 +54,6 @@ export class DonationItemsService { return items; } - async getAssociatedDonationIds( - donationItemIds: number[], - ): Promise> { - donationItemIds.forEach((id) => validateId(id, 'Donation Item')); - - const items = await this.repo.find({ - where: { itemId: In(donationItemIds) }, - select: ['itemId', 'donationId'], - }); - - const foundIds = new Set(items.map((i) => i.itemId)); - - const missingIds = donationItemIds.filter((id) => !foundIds.has(id)); - - if (missingIds.length > 0) { - throw new NotFoundException( - `Donation items not found for ID(s): ${missingIds.join(', ')}`, - ); - } - - return new Set(items.map((i) => i.donationId)); - } - async create( donationId: number, itemName: string, diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts index a19024d63..ae1072daf 100644 --- a/apps/backend/src/emails/emailTemplates.ts +++ b/apps/backend/src/emails/emailTemplates.ts @@ -154,7 +154,7 @@ export const emailTemplates = { ${EMAIL_REDIRECT_URL}/login

    - If you have any questions or feedback about this order, please do not hesitate to reach out. + If you have any questions or feedback about this request, please do not hesitate to reach out. You can contact your pantry coordinator, ${params.volunteerName}, at ${params.volunteerEmail}.

    @@ -177,7 +177,7 @@ 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 Items:

    +

    Matched Item(s):

      ${params.items .map((item) => `
    • ${item.quantity} of ${item.product}
    • `) @@ -185,6 +185,8 @@ export const emailTemplates = {

    Recipient Pantry: ${params.pantryName}
    +

    +

    Address:
    ${params.pantryAddress}

    diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 1fbb253d9..49daede45 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -319,7 +319,7 @@ export class RequestsService { relations: ['assignee'], }); - if (lastDeliveredOrder?.assignee) { + if (lastDeliveredOrder) { const { assignee } = lastDeliveredOrder; const message = emailTemplates.pantryRequestClosed({ pantryName: request.pantry.pantryName, @@ -331,6 +331,10 @@ export class RequestsService { message.subject, message.bodyHTML, ); + } else { + throw new InternalServerErrorException( + `Request ${requestId} auto-closed, but failed to send pantry notification email`, + ); } } catch { throw new InternalServerErrorException( diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index a828062bd..476b26317 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -327,7 +327,7 @@ export class OrdersService { pantryName: request.pantry.pantryName, items: itemDetails, brand: manufacturer.foodManufacturerName, - volunteerName: assignee.firstName + ' ' + assignee.lastName, + volunteerName: `${assignee.firstName} ${assignee.lastName}`, volunteerEmail: assignee.email, }); await this.emailsService.sendEmails( @@ -360,7 +360,7 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ items: itemDetails, pantryName: request.pantry.pantryName, pantryAddress: pantryAddress, - volunteerName: assignee.firstName + ' ' + assignee.lastName, + volunteerName: `${assignee.firstName} ${assignee.lastName}`, volunteerEmail: assignee.email, }); await this.emailsService.sendEmails( From 70055361e2d641b73a6d49e851f3d04eb5df2b9a Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Wed, 27 May 2026 10:59:50 -0400 Subject: [PATCH 17/20] comments --- .../src/foodRequests/request.service.spec.ts | 26 ++++++++------ .../src/foodRequests/request.service.ts | 27 ++++++++------ apps/backend/src/orders/order.service.spec.ts | 36 +++++++++---------- apps/backend/src/orders/order.service.ts | 22 ++++++------ 4 files changed, 61 insertions(+), 50 deletions(-) diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index 8c1d6cb7e..4e63aea09 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -427,12 +427,15 @@ describe('RequestsService', () => { volunteerEmail: assignee.email, }); + const volunteerEmails = (pantry.volunteers ?? []).map((v) => v.email); + expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); - expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( - [pantry.pantryUser.email], - expectedMessage.subject, - expectedMessage.bodyHTML, - ); + expect(mockEmailsService.sendEmails).toHaveBeenCalledWith({ + toEmail: pantry.pantryUser.email, + subject: expectedMessage.subject, + bodyHtml: expectedMessage.bodyHTML, + bccEmails: volunteerEmails, + }); }); it('does not send email when not all orders are delivered (request stays active)', async () => { @@ -900,12 +903,15 @@ describe('RequestsService', () => { volunteerEmail: `james.t@volunteer.org`, }); + const volunteerEmails = (pantry.volunteers ?? []).map((v) => v.email); + expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); - expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( - [pantry.pantryUser.email], - expectedMessage.subject, - expectedMessage.bodyHTML, - ); + expect(mockEmailsService.sendEmails).toHaveBeenCalledWith({ + toEmail: pantry.pantryUser.email, + subject: expectedMessage.subject, + bodyHtml: expectedMessage.bodyHTML, + bccEmails: volunteerEmails, + }); }); it('still closes request when email fails (manual close)', async () => { diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index f17f19b0a..90be8ebe4 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -325,17 +325,21 @@ export class RequestsService { }); if (lastDeliveredOrder) { + const volunteers = request.pantry.volunteers || []; + const volunteerEmails = volunteers.map((v) => v.email); + const { assignee } = lastDeliveredOrder; const message = emailTemplates.pantryRequestClosed({ pantryName: request.pantry.pantryName, volunteerName: `${assignee.firstName} ${assignee.lastName}`, volunteerEmail: assignee.email, }); - await this.emailsService.sendEmails( - [request.pantry.pantryUser.email], - message.subject, - message.bodyHTML, - ); + await this.emailsService.sendEmails({ + toEmail: request.pantry.pantryUser.email, + subject: message.subject, + bodyHtml: message.bodyHTML, + bccEmails: volunteerEmails, + }); } else { throw new InternalServerErrorException( `Request ${requestId} auto-closed, but failed to send pantry notification email`, @@ -441,16 +445,19 @@ export class RequestsService { request.status = FoodRequestStatus.CLOSED; const saved = await this.repo.save(request); try { + const volunteers = request.pantry.volunteers || []; + const volunteerEmails = volunteers.map((v) => v.email); const message = emailTemplates.pantryRequestClosed({ pantryName: request.pantry.pantryName, volunteerName: `${assignee.firstName} ${assignee.lastName}`, volunteerEmail: assignee.email, }); - await this.emailsService.sendEmails( - [request.pantry.pantryUser.email], - message.subject, - message.bodyHTML, - ); + await this.emailsService.sendEmails({ + toEmail: request.pantry.pantryUser.email, + subject: message.subject, + bodyHtml: message.bodyHTML, + bccEmails: volunteerEmails, + }); } catch { throw new InternalServerErrorException( 'Failed to send food request closed email to pantry', diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index a66f76226..7351a9bd6 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -635,7 +635,7 @@ describe('OrdersService', () => { fmName: order.foodManufacturer.foodManufacturerName, }); - expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(1); + expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2); expect(mockEmailsService.sendEmails).toHaveBeenCalledWith({ toEmail: order.assignee.email, subject: message.subject, @@ -656,7 +656,7 @@ describe('OrdersService', () => { ), ).rejects.toThrow( new InternalServerErrorException( - 'Failed to send order delivery confirmation email to volunteer', + 'Request 3 auto-closed, but failed to send pantry notification email', ), ); @@ -825,17 +825,17 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2); - expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( - [request.pantry.pantryUser.email], - pantryMessage.subject, - pantryMessage.bodyHTML, - ); + expect(mockEmailsService.sendEmails).toHaveBeenCalledWith({ + toEmail: request.pantry.pantryUser.email, + subject: pantryMessage.subject, + bodyHtml: pantryMessage.bodyHTML, + }); - expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( - [manufacturer.foodManufacturerRepresentative.email], - fmMessage.subject, - fmMessage.bodyHTML, - ); + expect(mockEmailsService.sendEmails).toHaveBeenCalledWith({ + toEmail: manufacturer.foodManufacturerRepresentative.email, + subject: fmMessage.subject, + bodyHtml: fmMessage.bodyHTML, + }); }); it('should throw BadRequestException if request is not active', async () => { @@ -998,15 +998,15 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ expect(mockEmailsService.sendEmails).toHaveBeenCalledTimes(2); const manufacturerRepo = testDataSource.getRepository(FoodManufacturer); - const manufacturer = await manufacturerRepo.findOne({ + const manufacturer = (await manufacturerRepo.findOne({ where: { foodManufacturerId: validCreateOrderDto.manufacturerId }, relations: ['foodManufacturerRepresentative'], + })) as FoodManufacturer; + expect(mockEmailsService.sendEmails).toHaveBeenCalledWith({ + toEmail: manufacturer.foodManufacturerRepresentative.email, + subject: expect.any(String), + bodyHtml: expect.any(String), }); - expect(mockEmailsService.sendEmails).toHaveBeenCalledWith( - [manufacturer!.foodManufacturerRepresentative.email], - expect.any(String), - expect.any(String), - ); }); it('should still create order when both emails fail', async () => { diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 7974be74b..830d5bd9e 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -4,7 +4,6 @@ import { InternalServerErrorException, Logger, NotFoundException, - InternalServerErrorException, } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { Repository, In, DataSource } from 'typeorm'; @@ -50,7 +49,6 @@ export class OrdersService { private donationItemsService: DonationItemsService, private allocationsService: AllocationsService, private donationService: DonationService, - private emailsService: EmailsService, @InjectDataSource() private dataSource: DataSource, private emailsService: EmailsService, ) {} @@ -335,11 +333,11 @@ export class OrdersService { volunteerName: `${assignee.firstName} ${assignee.lastName}`, volunteerEmail: assignee.email, }); - await this.emailsService.sendEmails( - [request.pantry.pantryUser.email], - pantryMessage.subject, - pantryMessage.bodyHTML, - ); + await this.emailsService.sendEmails({ + toEmail: request.pantry.pantryUser.email, + subject: pantryMessage.subject, + bodyHtml: pantryMessage.bodyHTML, + }); } catch { emailErrors.push( 'Failed to send pantry request matched order confirmation email', @@ -368,11 +366,11 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ volunteerName: `${assignee.firstName} ${assignee.lastName}`, volunteerEmail: assignee.email, }); - await this.emailsService.sendEmails( - [manufacturer.foodManufacturerRepresentative.email], - fmMessage.subject, - fmMessage.bodyHTML, - ); + await this.emailsService.sendEmails({ + toEmail: manufacturer.foodManufacturerRepresentative.email, + subject: fmMessage.subject, + bodyHtml: fmMessage.bodyHTML, + }); } catch { emailErrors.push( 'Failed to send food manufacturer donation matched order confirmation email', From 3bd4c4562b8a9e709a8f3f9e60cda2a7a328ab55 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Thu, 28 May 2026 13:58:04 -0400 Subject: [PATCH 18/20] comments --- apps/backend/src/orders/order.service.spec.ts | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 318eee9b4..2aee388f1 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -802,9 +802,9 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ const requestRepo = testDataSource.getRepository(FoodRequest); const donationItemRepo = testDataSource.getRepository(DonationItem); - const request = await requestRepo.findOne({ where: { requestId: 2 } }); - - if (!request) throw new Error('Missing dummy request'); + const request = (await requestRepo.findOne({ + where: { requestId: 2 }, + })) as FoodRequest; request.status = FoodRequestStatus.CLOSED; await requestRepo.save(request); @@ -817,10 +817,11 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ parsedAllocations, userId, ), - ).rejects.toMatchObject({ - name: BadRequestException.name, - message: `Request ${validCreateOrderDto.foodRequestId} is not active`, - }); + ).rejects.toThrow( + new BadRequestException( + `Request ${validCreateOrderDto.foodRequestId} is not active`, + ), + ); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -842,10 +843,11 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ parsedAllocations, userId, ), - ).rejects.toMatchObject({ - name: BadRequestException.name, - message: `Manufacturer ${validCreateOrderDto.manufacturerId} is not approved`, - }); + ).rejects.toThrow( + new BadRequestException( + `Manufacturer ${validCreateOrderDto.manufacturerId} is not approved`, + ), + ); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -854,10 +856,13 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ expect(donationItem1?.reservedQuantity).toBe(10); }); - it('should throw NotFoundException if donation item does not exist', async () => { + it('should throw BadRequestException if manufacturer has no donations', async () => { const donationItemRepo = testDataSource.getRepository(DonationItem); - parsedAllocations.set(999, 1); + await testDataSource.query( + `UPDATE donations SET food_manufacturer_id = 2 WHERE food_manufacturer_id = $1`, + [validCreateOrderDto.manufacturerId], + ); await expect( service.create( @@ -866,10 +871,11 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ parsedAllocations, userId, ), - ).rejects.toMatchObject({ - name: NotFoundException.name, - message: 'Donation items not found for ID(s): 999', - }); + ).rejects.toThrow( + new BadRequestException( + `Manufacturer ${validCreateOrderDto.manufacturerId} has no donations`, + ), + ); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -894,10 +900,11 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ parsedAllocations, userId, ), - ).rejects.toMatchObject({ - name: BadRequestException.name, - message: `Donation item ${donationItemId} quantity to allocate exceeds remaining quantity`, - }); + ).rejects.toThrow( + new BadRequestException( + `Donation item ${donationItemId} quantity to allocate exceeds remaining quantity`, + ), + ); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -906,7 +913,7 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ expect(donationItem1?.reservedQuantity).toBe(10); }); - it('should throw Error if donation is not associated with manufacturer', async () => { + it('should throw BadRequestException if donation is not associated with manufacturer', async () => { const donationItemRepo = testDataSource.getRepository(DonationItem); const donationItemId = 7; @@ -921,10 +928,11 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ parsedAllocations, userId, ), - ).rejects.toMatchObject({ - name: BadRequestException.name, - message: `The following donation items are not associated with the current food manufacturer: Donation item ID ${donationItemId} with Donation ID 3`, - }); + ).rejects.toThrow( + new BadRequestException( + `The following donation items are not associated with the current food manufacturer: Donation item ID ${donationItemId} with Donation ID 3`, + ), + ); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -1128,10 +1136,9 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ emptyAllocations, userId, ), - ).rejects.toMatchObject({ - name: BadRequestException.name, - message: 'Cannot create order with no donation items', - }); + ).rejects.toThrow( + new BadRequestException('Cannot create order with no donation items'), + ); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ @@ -1151,10 +1158,9 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ parsedAllocations, userId, ), - ).rejects.toMatchObject({ - name: NotFoundException.name, - message: `Request ${nonExistentRequestId} not found`, - }); + ).rejects.toThrow( + new NotFoundException(`Request ${nonExistentRequestId} not found`), + ); // Asserting that donation item reserved quantity wasn't updated const donationItem1 = await donationItemRepo.findOne({ From c1db6c65b8a2c0a54bd38523db3a017af7428bb9 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 2 Jun 2026 14:55:40 -0400 Subject: [PATCH 19/20] comment --- apps/backend/src/emails/emailTemplates.ts | 2 +- .../src/foodRequests/request.service.spec.ts | 42 +++++++++++-------- .../src/foodRequests/request.service.ts | 27 ++++++------ 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts index 28c224368..e24b716e1 100644 --- a/apps/backend/src/emails/emailTemplates.ts +++ b/apps/backend/src/emails/emailTemplates.ts @@ -4,7 +4,7 @@ export type EmailTemplate = { additionalContent?: string; }; -export const EMAIL_REDIRECT_URL = 'https://localhost:4200'; +export const EMAIL_REDIRECT_URL = 'http://localhost:4200'; // TODO: Change this before production to be the actual ssf email export const SSF_PARTNER_EMAIL = 'example@gmail.com'; diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index 4e63aea09..ca13422c5 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -376,7 +376,7 @@ describe('RequestsService', () => { expect(request.status).toBe(FoodRequestStatus.ACTIVE); }); - it('should update status to active for request with no orders', async () => { + it('should throw BadRequestException for request with no orders', async () => { const pantryId = 1; const result = await service.create(pantryId, RequestSize.MEDIUM, [ FoodType.DRIED_BEANS, @@ -384,10 +384,11 @@ describe('RequestsService', () => { ]); const requestId = result.requestId; - await service.updateRequestStatus(requestId); - - const request = await service.findOne(requestId); - expect(request.status).toBe(FoodRequestStatus.ACTIVE); + await expect(service.updateRequestStatus(requestId)).rejects.toThrow( + new BadRequestException( + `Cannot update request ${requestId} with no orders`, + ), + ); }); it('should throw NotFoundException for non-existent request', async () => { @@ -402,7 +403,7 @@ describe('RequestsService', () => { const requestId = 1; const pantry = (await testDataSource.getRepository(Pantry).findOne({ where: { pantryId: 1 }, - relations: ['pantryUser'], + relations: ['pantryUser', 'volunteers'], })) as Pantry; const lastDeliveredOrder = (await testDataSource .getRepository(Order) @@ -451,7 +452,7 @@ describe('RequestsService', () => { expect(mockEmailsService.sendEmails).not.toHaveBeenCalled(); }); - it('does not send email when request was already closed before updateRequestStatus', async () => { + it('throws BadRequestException and does not send email when request is already closed', async () => { await testDataSource.query( `UPDATE food_requests SET status = 'closed' WHERE request_id = 1`, ); @@ -459,7 +460,9 @@ describe('RequestsService', () => { const request = await service.findOne(1); expect(request.status).toBe(FoodRequestStatus.CLOSED); - await service.updateRequestStatus(1); + await expect(service.updateRequestStatus(1)).rejects.toThrow( + new BadRequestException(`Request 1 is already closed`), + ); expect(mockEmailsService.sendEmails).not.toHaveBeenCalled(); }); @@ -481,6 +484,17 @@ describe('RequestsService', () => { const request = await service.findOne(1); expect(request.status).toBe(FoodRequestStatus.CLOSED); }); + + it('should not reopen a closed request when updateRequestStatus is called', async () => { + await service.closeRequest(1, 6); + + await expect(service.updateRequestStatus(1)).rejects.toThrow( + new BadRequestException(`Request 1 is already closed`), + ); + + const fromDb = await service.findOne(1); + expect(fromDb.status).toBe(FoodRequestStatus.CLOSED); + }); }); describe('getMatchingManufacturers', () => { @@ -881,18 +895,10 @@ describe('RequestsService', () => { }); }); - it('should not reopen a closed request when updateRequestStatus is called', async () => { - await service.closeRequest(1, volunteerId); - await service.updateRequestStatus(1); - - const fromDb = await service.findOne(1); - expect(fromDb.status).toBe(FoodRequestStatus.CLOSED); - }); - it('sends pantry closed email with acting volunteer info on successful close', async () => { const pantry = (await testDataSource.getRepository(Pantry).findOne({ where: { pantryId: 3 }, - relations: ['pantryUser'], + relations: ['pantryUser', 'volunteers'], })) as Pantry; await service.closeRequest(3, volunteerId); @@ -910,7 +916,7 @@ describe('RequestsService', () => { toEmail: pantry.pantryUser.email, subject: expectedMessage.subject, bodyHtml: expectedMessage.bodyHTML, - bccEmails: volunteerEmails, + bccEmails: expect.arrayContaining(volunteerEmails), }); }); diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 90be8ebe4..69f2b3767 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -287,7 +287,7 @@ export class RequestsService { const request = await this.repo.findOne({ where: { requestId }, - relations: ['orders', 'pantry', 'pantry.pantryUser'], + relations: ['orders', 'pantry', 'pantry.pantryUser', 'pantry.volunteers'], }); if (!request) { @@ -297,26 +297,26 @@ export class RequestsService { const orders = request.orders || []; if (!orders.length) { - request.status = FoodRequestStatus.ACTIVE; - await this.repo.save(request); - return; + throw new BadRequestException( + `Cannot update request ${requestId} with no orders`, + ); + } + + if (request.status === FoodRequestStatus.CLOSED) { + throw new BadRequestException(`Request ${requestId} is already closed`); } const allDelivered = orders.every( (order) => order.status === OrderStatus.DELIVERED, ); - const wasAlreadyClosed = request.status === FoodRequestStatus.CLOSED; - - if (!wasAlreadyClosed) { - request.status = allDelivered - ? FoodRequestStatus.CLOSED - : FoodRequestStatus.ACTIVE; - } + request.status = allDelivered + ? FoodRequestStatus.CLOSED + : FoodRequestStatus.ACTIVE; await this.repo.save(request); - if (allDelivered && !wasAlreadyClosed) { + if (allDelivered) { try { const lastDeliveredOrder = await this.orderRepo.findOne({ where: { requestId, status: OrderStatus.DELIVERED }, @@ -427,7 +427,7 @@ export class RequestsService { const request = await this.repo.findOne({ where: { requestId }, - relations: ['pantry', 'pantry.pantryUser'], + relations: ['pantry', 'pantry.pantryUser', 'pantry.volunteers'], }); if (!request) { @@ -447,6 +447,7 @@ export class RequestsService { try { const volunteers = request.pantry.volunteers || []; const volunteerEmails = volunteers.map((v) => v.email); + console.log(volunteerEmails); const message = emailTemplates.pantryRequestClosed({ pantryName: request.pantry.pantryName, volunteerName: `${assignee.firstName} ${assignee.lastName}`, From c88f4b339d7f4324bfca376b5fb8fec2c84bb168 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Tue, 2 Jun 2026 14:56:42 -0400 Subject: [PATCH 20/20] remove console log --- apps/backend/src/foodRequests/request.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 69f2b3767..c9f17b52e 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -447,7 +447,6 @@ export class RequestsService { try { const volunteers = request.pantry.volunteers || []; const volunteerEmails = volunteers.map((v) => v.email); - console.log(volunteerEmails); const message = emailTemplates.pantryRequestClosed({ pantryName: request.pantry.pantryName, volunteerName: `${assignee.firstName} ${assignee.lastName}`,