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}`,