Skip to content

Commit 03b3177

Browse files
committed
Add cancellation email notifications for matches
- Add email methods to SESEmailService for participant and volunteer cancellations - Create email templates (EN/FR) for both cancellation types - Update match_service to send emails when matches are cancelled - Include scheduled call date/time in cancellation emails - Register new templates in ses_templates.json
1 parent fa9f245 commit 03b3177

15 files changed

+1240
-1
lines changed

backend/app/services/implementations/match_service.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,62 @@ async def cancel_match_by_participant(
483483

484484
self._set_match_status(match, "cancelled_by_participant")
485485

486-
# TODO: send particpant an email saying that the match has been cancelled
486+
# Send cancellation email to volunteer before deleting match
487+
try:
488+
# Load volunteer with their data before deleting match
489+
volunteer = (
490+
self.db.query(User)
491+
.options(joinedload(User.user_data))
492+
.filter(User.id == match.volunteer_id)
493+
.first()
494+
)
495+
participant = (
496+
self.db.query(User)
497+
.options(joinedload(User.user_data))
498+
.filter(User.id == match.participant_id)
499+
.first()
500+
)
501+
502+
if volunteer and participant and match.confirmed_time:
503+
ses_service = SESEmailService()
504+
confirmed_time_utc = match.confirmed_time.start_time
505+
506+
# Get volunteer's timezone and language
507+
volunteer_tz = ZoneInfo("America/Toronto") # Default to EST
508+
if volunteer.user_data and volunteer.user_data.timezone:
509+
tz_result = get_timezone_from_abbreviation(volunteer.user_data.timezone)
510+
if tz_result:
511+
volunteer_tz = tz_result
512+
513+
volunteer_language = volunteer.language.value if volunteer.language else "en"
514+
515+
# Convert time to volunteer's timezone
516+
volunteer_time = confirmed_time_utc.astimezone(volunteer_tz)
517+
volunteer_date = volunteer_time.strftime("%B %d, %Y")
518+
volunteer_time_str = volunteer_time.strftime("%I:%M %p")
519+
volunteer_tz_abbr = volunteer_time.strftime("%Z")
520+
521+
# Send to volunteer
522+
if volunteer.email:
523+
participant_name = (
524+
f"{participant.first_name} {participant.last_name}"
525+
if participant.first_name and participant.last_name
526+
else participant.first_name or "The participant"
527+
)
528+
ses_service.send_participant_cancelled_email(
529+
to_email=volunteer.email,
530+
participant_name=participant_name,
531+
date=volunteer_date,
532+
time=volunteer_time_str,
533+
timezone=volunteer_tz_abbr,
534+
first_name=volunteer.first_name,
535+
dashboard_url="http://localhost:3000/volunteer/dashboard",
536+
language=volunteer_language,
537+
)
538+
except Exception as e:
539+
# Log error but don't fail the cancellation
540+
self.logger.error(f"Failed to send participant cancelled email for match {match_id}: {e}")
541+
487542
# Soft-delete the match when cancelled (cleans up time blocks and sets deleted_at)
488543
self._delete_match(match)
489544

@@ -519,6 +574,62 @@ async def cancel_match_by_volunteer(
519574
if acting_volunteer_id and match.volunteer_id != acting_volunteer_id:
520575
raise HTTPException(status_code=403, detail="Cannot modify another volunteer's match")
521576

577+
# Send cancellation email to participant before clearing confirmed time
578+
try:
579+
# Load participant and volunteer with their data before clearing time
580+
participant = (
581+
self.db.query(User)
582+
.options(joinedload(User.user_data))
583+
.filter(User.id == match.participant_id)
584+
.first()
585+
)
586+
volunteer = (
587+
self.db.query(User)
588+
.options(joinedload(User.user_data))
589+
.filter(User.id == match.volunteer_id)
590+
.first()
591+
)
592+
593+
if participant and volunteer and match.confirmed_time:
594+
ses_service = SESEmailService()
595+
confirmed_time_utc = match.confirmed_time.start_time
596+
597+
# Get participant's timezone and language
598+
participant_tz = ZoneInfo("America/Toronto") # Default to EST
599+
if participant.user_data and participant.user_data.timezone:
600+
tz_result = get_timezone_from_abbreviation(participant.user_data.timezone)
601+
if tz_result:
602+
participant_tz = tz_result
603+
604+
participant_language = participant.language.value if participant.language else "en"
605+
606+
# Convert time to participant's timezone
607+
participant_time = confirmed_time_utc.astimezone(participant_tz)
608+
participant_date = participant_time.strftime("%B %d, %Y")
609+
participant_time_str = participant_time.strftime("%I:%M %p")
610+
participant_tz_abbr = participant_time.strftime("%Z")
611+
612+
# Send to participant
613+
if participant.email:
614+
volunteer_name = (
615+
f"{volunteer.first_name} {volunteer.last_name}"
616+
if volunteer.first_name and volunteer.last_name
617+
else volunteer.first_name or "Your volunteer"
618+
)
619+
ses_service.send_volunteer_cancelled_email(
620+
to_email=participant.email,
621+
volunteer_name=volunteer_name,
622+
date=participant_date,
623+
time=participant_time_str,
624+
timezone=participant_tz_abbr,
625+
first_name=participant.first_name,
626+
request_matches_url="http://localhost:3000/participant/dashboard",
627+
language=participant_language,
628+
)
629+
except Exception as e:
630+
# Log error but don't fail the cancellation
631+
self.logger.error(f"Failed to send volunteer cancelled email for match {match_id}: {e}")
632+
522633
self._clear_confirmed_time(match)
523634
self._set_match_status(match, "cancelled_by_volunteer")
524635

backend/app/utilities/ses/ses_templates.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,29 @@
8888
"SubjectPart": "Confirmation de la nouvelle plage horaire, le {{date}} à {{time}} {{timezone}}, par {{volunteer_name}} – Programme de soutien par les pairs Premier contact",
8989
"TemplateName": "VolunteerAcceptedNewTimesFr",
9090
"TextPart": "app/utilities/ses/template_files/text/volunteer_accepted_new_times_fr.txt"
91+
},
92+
{
93+
"HtmlPart": "app/utilities/ses/template_files/compiled/participant_cancelled_en.html",
94+
"SubjectPart": "Call cancelled by {{participant_name}} - First Connection Peer Support Program",
95+
"TemplateName": "ParticipantCancelledEn",
96+
"TextPart": "app/utilities/ses/template_files/text/participant_cancelled_en.txt"
97+
},
98+
{
99+
"HtmlPart": "app/utilities/ses/template_files/compiled/participant_cancelled_fr.html",
100+
"SubjectPart": "Appel annulé par {{participant_name}} – Programme de soutien par les pairs Premier contact",
101+
"TemplateName": "ParticipantCancelledFr",
102+
"TextPart": "app/utilities/ses/template_files/text/participant_cancelled_fr.txt"
103+
},
104+
{
105+
"HtmlPart": "app/utilities/ses/template_files/compiled/volunteer_cancelled_en.html",
106+
"SubjectPart": "Call cancelled by {{volunteer_name}} - First Connection Peer Support Program",
107+
"TemplateName": "VolunteerCancelledEn",
108+
"TextPart": "app/utilities/ses/template_files/text/volunteer_cancelled_en.txt"
109+
},
110+
{
111+
"HtmlPart": "app/utilities/ses/template_files/compiled/volunteer_cancelled_fr.html",
112+
"SubjectPart": "Appel annulé par {{volunteer_name}} – Programme de soutien par les pairs Premier contact",
113+
"TemplateName": "VolunteerCancelledFr",
114+
"TextPart": "app/utilities/ses/template_files/text/volunteer_cancelled_fr.txt"
91115
}
92116
]

0 commit comments

Comments
 (0)