From e1fd3afddacc7ed8f9b29ef78c0d7f73125875cf Mon Sep 17 00:00:00 2001 From: skewalia <145241312+swarkewalia@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:08:56 -0400 Subject: [PATCH 1/2] automated email updates --- .../backend/src/donations/donations.module.ts | 2 + .../src/donations/donations.service.ts | 48 +++++++++---- apps/backend/src/emails/emailTemplates.ts | 70 +++++++++++++++++++ apps/backend/src/orders/order.module.ts | 2 + apps/backend/src/orders/order.service.ts | 42 ++++++++++- 5 files changed, 148 insertions(+), 16 deletions(-) diff --git a/apps/backend/src/donations/donations.module.ts b/apps/backend/src/donations/donations.module.ts index c3a4de760..3acfed5ac 100644 --- a/apps/backend/src/donations/donations.module.ts +++ b/apps/backend/src/donations/donations.module.ts @@ -10,6 +10,7 @@ import { DonationItem } from '../donationItems/donationItems.entity'; import { DonationItemsModule } from '../donationItems/donationItems.module'; import { Allocation } from '../allocations/allocations.entity'; import { AllocationModule } from '../allocations/allocations.module'; +import { EmailsModule } from '../emails/email.module'; @Module({ imports: [ @@ -22,6 +23,7 @@ import { AllocationModule } from '../allocations/allocations.module'; forwardRef(() => AuthModule), DonationItemsModule, AllocationModule, + EmailsModule, ], controllers: [DonationsController], providers: [DonationService, DonationsSchedulerService], diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index ef638e884..5db01d9f3 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable, - Logger, NotFoundException, } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; @@ -15,11 +14,11 @@ import { DonationItemsService } from '../donationItems/donationItems.service'; import { ReplaceDonationItemsDto } from '../donationItems/dtos/create-donation-items.dto'; import { DonationItem } from '../donationItems/donationItems.entity'; import { Allocation } from '../allocations/allocations.entity'; +import { EmailsService } from '../emails/email.service'; +import { emailTemplates } from '../emails/emailTemplates'; @Injectable() export class DonationService { - private readonly logger = new Logger(DonationService.name); - constructor( @InjectRepository(Donation) private repo: Repository, @InjectRepository(Allocation) @@ -30,6 +29,7 @@ export class DonationService { private manufacturerRepo: Repository, private donationItemsService: DonationItemsService, @InjectDataSource() private dataSource: DataSource, + private emailsService: EmailsService, ) {} async findOne(donationId: number): Promise { @@ -203,13 +203,22 @@ export class DonationService { break; } - this.logger.log(`Placeholder for sending automated email`); + const { subject, bodyHTML } = + emailTemplates.fmRecurringDonationReminder({ + fmName: donation.foodManufacturer.foodManufacturerName, + }); + + try { + const fmEmails = [ + donation.foodManufacturer.secondaryContactEmail, + ].filter((e): e is string => e !== null); - /** - * IMPORTANT: future logic below should only proceed if the email is successfully sent - */ - const emailSent = true; - if (!emailSent) continue; + if (fmEmails.length > 0) { + await this.emailsService.sendEmails(fmEmails, subject, bodyHTML); + } + } catch (e) { + continue; + } dates.splice(i, 1); i--; @@ -225,11 +234,22 @@ export class DonationService { // cascading recalculation of next dates when replacement dates are also expired while (nextDate.getTime() <= today.getTime() && occurrences > 0) { - this.logger.log( - `Placeholder for sending automated email for replacement date`, - ); - const cascadeEmailSent = true; - if (!cascadeEmailSent) break; + const { subject: cs, bodyHTML: cb } = + emailTemplates.fmRecurringDonationReminder({ + fmName: donation.foodManufacturer.foodManufacturerName, + }); + + try { + const fmEmails = [ + donation.foodManufacturer.secondaryContactEmail, + ].filter((e): e is string => e !== null); + + if (fmEmails.length > 0) { + await this.emailsService.sendEmails(fmEmails, cs, cb); + } + } catch (e) { + break; + } occurrences -= 1; diff --git a/apps/backend/src/emails/emailTemplates.ts b/apps/backend/src/emails/emailTemplates.ts index 7d4b8c0f3..41c6350ec 100644 --- a/apps/backend/src/emails/emailTemplates.ts +++ b/apps/backend/src/emails/emailTemplates.ts @@ -98,4 +98,74 @@ export const emailTemplates = {

Best regards,
The Securing Safe Food Team

`, }), + + fmRecurringDonationReminder: (params: { fmName: string }): EmailTemplate => ({ + subject: 'Reminder: Submit Your Scheduled Recurring Donation with SSF', + bodyHTML: ` +

Hi ${params.fmName},

+

+ This is a friendly reminder from Securing Safe Food that your recurring donation + schedule indicates a new donation submission is due. +

+

+ When you have a moment, please log into your account and submit your current + donation availability so we can continue matching your contributions with pantry requests. +

+

+ We greatly appreciate your continued generosity and support of our mission. Your + recurring donations make a meaningful and consistent impact for the communities we serve. +

+

Best regards,
The Securing Safe Food Team

+ `, + }), + + trackingLinkAvailable: (params: { + pantryName: string; + fmName: string; + trackingLink: string; + volunteerName: string; + volunteerEmail: string; + }): EmailTemplate => ({ + subject: `Tracking Information for your ${params.fmName} delivery (Securing Safe Food)`, + bodyHTML: ` +

Hi ${params.pantryName},

+

+ Good news! Tracking information is now available for your upcoming SSF delivery + from ${params.fmName}. 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

+ `, + }), + + pantryConfirmsOrderDelivery: (params: { + volunteerName: string; + pantryName: string; + fmName: string; + }): EmailTemplate => ({ + subject: `${params.pantryName} Confirmed for your ${params.fmName} Order`, + bodyHTML: ` +

Hi ${params.volunteerName},

+

+ ${params.pantryName} has confirmed receipt of the most recent ${params.fmName} + order you are assigned to. Please log into the platform to review the completed + request or check for additional information. +

+

+ Thank you for your coordination and support in helping reach this order to completion! +

+

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.ts b/apps/backend/src/orders/order.service.ts index 378718e0e..0fa0cc21c 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -23,6 +23,8 @@ 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 { emailTemplates } from '../emails/emailTemplates'; @Injectable() export class OrdersService { @@ -36,6 +38,7 @@ export class OrdersService { private allocationsService: AllocationsService, private donationService: DonationService, @InjectDataSource() private dataSource: DataSource, + private emailsService: EmailsService, ) {} // TODO: when order is created, set FM @@ -391,6 +394,7 @@ export class OrdersService { .execute(); } + // Updated confirmDelivery() async confirmDelivery( orderId: number, dto: ConfirmDeliveryDto, @@ -403,7 +407,10 @@ export class OrdersService { throw new BadRequestException('Invalid date format for dateReceived'); } - const order = await this.repo.findOneBy({ orderId }); + const order = await this.repo.findOne({ + where: { orderId }, + relations: ['request', 'request.pantry', 'foodManufacturer', 'assignee'], + }); if (!order) { throw new NotFoundException(`Order ${orderId} not found`); @@ -424,6 +431,18 @@ export class OrdersService { await this.requestsService.updateRequestStatus(order.requestId); + const { subject, bodyHTML } = emailTemplates.pantryConfirmsOrderDelivery({ + volunteerName: `${order.assignee.firstName} ${order.assignee.lastName}`, + pantryName: order.request.pantry.pantryName, + fmName: order.foodManufacturer.foodManufacturerName, + }); + + await this.emailsService.sendEmails( + [order.assignee.email], + subject, + bodyHTML, + ); + return updatedOrder; } @@ -472,7 +491,10 @@ export class OrdersService { dto.trackingLink = sanitized; } - const order = await this.repo.findOneBy({ orderId }); + const order = await this.repo.findOne({ + where: { orderId }, + relations: ['request', 'request.pantry', 'foodManufacturer', 'assignee'], + }); if (!order) { throw new NotFoundException(`Order ${orderId} not found`); } @@ -504,6 +526,22 @@ export class OrdersService { ) { order.status = OrderStatus.SHIPPED; order.shippedAt = new Date(); + + const { subject, bodyHTML } = emailTemplates.trackingLinkAvailable({ + pantryName: order.request.pantry.pantryName, + fmName: order.foodManufacturer.foodManufacturerName, + trackingLink: order.trackingLink, + volunteerName: `${order.assignee.firstName} ${order.assignee.lastName}`, + volunteerEmail: order.assignee.email, + }); + + const pantryEmails = [order.request.pantry.secondaryContactEmail].filter( + (e): e is string => e !== null, + ); + + if (pantryEmails.length > 0) { + await this.emailsService.sendEmails(pantryEmails, subject, bodyHTML); + } } await this.repo.save(order); From de35710daa1ce3e4ceda35aad36ec689d566747e Mon Sep 17 00:00:00 2001 From: skewalia <145241312+swarkewalia@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:50:21 -0400 Subject: [PATCH 2/2] fix donation tests --- apps/backend/src/donations/donations.service.spec.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index b37427025..c891ef4e0 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -16,6 +16,7 @@ import { import { FoodType } from '../donationItems/types'; import { DonationItemsService } from '../donationItems/donationItems.service'; import { DonationItem } from '../donationItems/donationItems.entity'; +import { EmailsService } from '../emails/email.service'; jest.setTimeout(60000); @@ -126,6 +127,10 @@ describe('DonationService', () => { provide: DataSource, useValue: testDataSource, }, + { + provide: EmailsService, + useValue: { sendEmails: jest.fn() }, + }, ], }).compile();