Skip to content
86 changes: 86 additions & 0 deletions apps/backend/src/emails/emailTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,90 @@ export const emailTemplates = {
<p>Best regards,<br />The Securing Safe Food Team</p>
`,
}),

pantryRequestMatchedOrder: (params: {
pantryName: string;
items: { quantity: string; product: string }[];
brand: string;
volunteerName: string;
volunteerEmail: string;
}): EmailTemplate => ({
subject: 'Your Securing Safe Food Request Has Been Matched to a Delivery',
bodyHTML: `
<p>Hi ${params.pantryName},</p>
<p>
Good news! Your recent food request through Securing Safe Food has been successfully matched to an order and is now moving forward toward delivery.
</p>
<p>
<strong>Items you will receive from ${params.brand}:</strong>
<ul>
${params.items
.map((item) => `<li>${item.quantity} of ${item.product}</li>`)
.join('')}
</ul>
</p>
<p>
To view full order details, delivery updates, and any notes from the coordinating volunteer or food manufacturer, please log into the platform.
</p>
<p>
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}.
</p>
<p>
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!
</p>
<p>Best regards,<br />The Securing Safe Food Team</p>
<p>
To log in to your account, please click the following link: <a href="${EMAIL_REDIRECT_URL}/login">${EMAIL_REDIRECT_URL}/login</a>
</p>
`,
}),

fmDonationMatchedOrder: (params: {
manufacturerName: string;
items: { 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: `
<p>Hi ${params.manufacturerName},</p>
<p>
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.
</p>
<p>
<strong>Matched Items:</strong><br />
<ul>
${params.items
.map((item) => `<li>${item.quantity} of ${item.product}</li>`)
.join('')}
</ul>
<strong>Recipient Pantry:</strong> ${params.pantryName}<br />
<strong>Address:</strong><br />
${params.pantryAddress}
</p>
<p>
Please log into the platform to review the full delivery details, timelines, and any special handling instructions associated with this shipment.
</p>
<p>
Your support plays a direct role in expanding access to allergen-safe foods, and we truly appreciate your commitment to this work.
</p>
<p>
If you have any questions or need assistance, please contact your coordinator, ${
params.volunteerName
} at ${params.volunteerEmail}.
</p>
<p>
Thank you so much.
</p>
<p>Best regards,<br />The Securing Safe Food Team</p>
<p>
To log in to your account, please click the following link: <a href="${EMAIL_REDIRECT_URL}/login">${EMAIL_REDIRECT_URL}/login</a>
</p>
`,
}),
};
6 changes: 6 additions & 0 deletions apps/backend/src/orders/order.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { ManufacturerModule } from '../foodManufacturers/manufacturers.module';
import { DonationItemsModule } from '../donationItems/donationItems.module';
import { Allocation } from '../allocations/allocations.entity';
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: [
Expand All @@ -28,6 +31,7 @@ import { Donation } from '../donations/donations.entity';
DonationItem,
Allocation,
Donation,
User,
]),
AllocationModule,
forwardRef(() => AuthModule),
Expand All @@ -37,6 +41,8 @@ import { Donation } from '../donations/donations.entity';
ManufacturerModule,
DonationItemsModule,
DonationModule,
EmailsModule,
forwardRef(() => UsersModule),
],
controllers: [OrdersController],
providers: [OrdersService],
Expand Down
78 changes: 77 additions & 1 deletion apps/backend/src/orders/order.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,20 @@ import { CreateOrderDto } from './dtos/create-order.dto';
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);

const mockEmailsService = mock<EmailsService>();

describe('OrdersService', () => {
let service: OrdersService;

beforeAll(async () => {
mockEmailsService.sendEmails.mockResolvedValue(undefined);

// Initialize DataSource once
if (!testDataSource.isInitialized) {
await testDataSource.initialize();
Expand Down Expand Up @@ -106,13 +112,18 @@ describe('OrdersService', () => {
provide: AuthService,
useValue: {},
},
{
provide: EmailsService,
useValue: mockEmailsService,
},
],
}).compile();

service = module.get<OrdersService>(OrdersService);
});

beforeEach(async () => {
mockEmailsService.sendEmails.mockClear();
await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`);
await testDataSource.query(`CREATE SCHEMA public`);
await testDataSource.runMigrations();
Expand Down Expand Up @@ -789,10 +800,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);

Expand Down Expand Up @@ -877,6 +891,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 () => {
Expand Down
Loading
Loading