Skip to content

Commit b226ae3

Browse files
YashK2005claude
andcommitted
Implement automated email sending with bilingual support
- Add intake form confirmation email on form submission - Add password reset email with language detection from user DB - Send matches available email to volunteer when admin creates match - Send matches available email to participant when volunteer sends availability - Send call scheduled email to both users with timezone conversion - Send participant requested new times email to volunteer - Add missing __init__.py files for proper package structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 17c676f commit b226ae3

File tree

5 files changed

+183
-5
lines changed

5 files changed

+183
-5
lines changed

backend/app/routes/intake.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from app.schemas.user import UserRole
1414
from app.services.implementations.form_processor import FormProcessor
1515
from app.utilities.db_utils import get_db
16+
from app.utilities.ses_email_service import SESEmailService
1617

1718
# ===== Schemas =====
1819

@@ -269,6 +270,21 @@ async def create_form_submission(
269270
db.commit()
270271
db.refresh(db_submission)
271272

273+
# Send intake form confirmation email for intake forms
274+
if form and form.type == "intake":
275+
try:
276+
ses_service = SESEmailService()
277+
# Get language (enum values are already "en" or "fr")
278+
language = target_user.language.value if target_user.language else "en"
279+
280+
first_name = target_user.first_name if target_user.first_name else None
281+
ses_service.send_intake_form_confirmation_email(
282+
to_email=target_user.email, first_name=first_name, language=language
283+
)
284+
except Exception as e:
285+
# Log error but don't fail the request
286+
print(f"Failed to send intake form confirmation email: {str(e)}")
287+
272288
# Build response dict
273289
response_dict = {
274290
"id": db_submission.id,

backend/app/services/implementations/auth_service.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,19 @@ def renew_token(self, refresh_token: str) -> Token:
4949

5050
def reset_password(self, email: str) -> None:
5151
try:
52-
# Get user's first name if available
52+
# Get user's first name and language if available
5353
first_name = None
54+
language = "en" # Default to English
5455
try:
5556
# Try database first, then fall back to Firebase
5657
try:
5758
user = self.user_service.get_user_by_email(email)
58-
if user and user.first_name and user.first_name.strip():
59-
first_name = user.first_name.strip()
59+
if user:
60+
if user.first_name and user.first_name.strip():
61+
first_name = user.first_name.strip()
62+
# Get language from user (enum values are already "en" or "fr")
63+
if user.language:
64+
language = user.language.value
6065
except Exception:
6166
pass
6267

@@ -80,8 +85,8 @@ def reset_password(self, email: str) -> None:
8085

8186
reset_link = firebase_admin.auth.generate_password_reset_link(email, action_code_settings)
8287

83-
# Send via SES
84-
email_sent = self.ses_email_service.send_password_reset_email(email, reset_link, first_name)
88+
# Send via SES with language
89+
email_sent = self.ses_email_service.send_password_reset_email(email, reset_link, first_name, language)
8590

8691
if email_sent:
8792
self.logger.info(f"Password reset email sent successfully to {email}")

backend/app/services/implementations/match_service.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
)
2525
from app.schemas.time_block import TimeBlockEntity, TimeRange
2626
from app.schemas.user import UserRole
27+
from app.utilities.ses_email_service import SESEmailService
2728
from app.utilities.timezone_utils import get_timezone_from_abbreviation
2829

2930
SCHEDULE_CLEANUP_STATUSES = {
@@ -95,6 +96,28 @@ async def create_matches(self, req: MatchCreateRequest) -> MatchCreateResponse:
9596
for match in created_matches:
9697
self.db.refresh(match)
9798

99+
# Send "matches available" email to each volunteer
100+
ses_service = SESEmailService()
101+
for match in created_matches:
102+
try:
103+
volunteer = self.db.get(User, match.volunteer_id)
104+
if volunteer and volunteer.email:
105+
# Get volunteer's language (enum values are already "en" or "fr")
106+
language = volunteer.language.value if volunteer.language else "en"
107+
108+
first_name = volunteer.first_name if volunteer.first_name else None
109+
matches_url = "http://localhost:3000/volunteer/dashboard"
110+
111+
ses_service.send_matches_available_email(
112+
to_email=volunteer.email,
113+
first_name=first_name,
114+
matches_url=matches_url,
115+
language=language,
116+
)
117+
except Exception as e:
118+
# Log error but don't fail the match creation
119+
self.logger.error(f"Failed to send matches available email to volunteer {match.volunteer_id}: {e}")
120+
98121
responses = [self._build_match_response(match) for match in created_matches]
99122
return MatchCreateResponse(matches=responses)
100123

@@ -258,6 +281,90 @@ async def schedule_match(
258281
self.db.commit()
259282
self.db.refresh(match)
260283

284+
# Send "call scheduled" email to both participant and volunteer
285+
try:
286+
# Load participant and volunteer with their data
287+
participant = (
288+
self.db.query(User)
289+
.options(joinedload(User.user_data))
290+
.filter(User.id == match.participant_id)
291+
.first()
292+
)
293+
volunteer = (
294+
self.db.query(User)
295+
.options(joinedload(User.user_data))
296+
.filter(User.id == match.volunteer_id)
297+
.first()
298+
)
299+
300+
if participant and volunteer and match.confirmed_time:
301+
ses_service = SESEmailService()
302+
confirmed_time_utc = match.confirmed_time.start_time
303+
304+
# Get participant's timezone and language
305+
participant_tz = ZoneInfo("America/Toronto") # Default to EST
306+
if participant.user_data and participant.user_data.timezone:
307+
tz_result = get_timezone_from_abbreviation(participant.user_data.timezone)
308+
if tz_result:
309+
participant_tz = tz_result
310+
311+
participant_language = participant.language.value if participant.language else "en"
312+
313+
# Get volunteer's timezone and language
314+
volunteer_tz = ZoneInfo("America/Toronto") # Default to EST
315+
if volunteer.user_data and volunteer.user_data.timezone:
316+
tz_result = get_timezone_from_abbreviation(volunteer.user_data.timezone)
317+
if tz_result:
318+
volunteer_tz = tz_result
319+
320+
volunteer_language = volunteer.language.value if volunteer.language else "en"
321+
322+
# Convert time to participant's timezone
323+
participant_time = confirmed_time_utc.astimezone(participant_tz)
324+
participant_date = participant_time.strftime("%B %d, %Y")
325+
participant_time_str = participant_time.strftime("%I:%M %p")
326+
participant_tz_abbr = participant_time.strftime("%Z")
327+
328+
# Convert time to volunteer's timezone
329+
volunteer_time = confirmed_time_utc.astimezone(volunteer_tz)
330+
volunteer_date = volunteer_time.strftime("%B %d, %Y")
331+
volunteer_time_str = volunteer_time.strftime("%I:%M %p")
332+
volunteer_tz_abbr = volunteer_time.strftime("%Z")
333+
334+
# Send to participant
335+
if participant.email:
336+
ses_service.send_call_scheduled_email(
337+
to_email=participant.email,
338+
match_name=f"{volunteer.first_name} {volunteer.last_name}"
339+
if volunteer.first_name and volunteer.last_name
340+
else "Your volunteer",
341+
date=participant_date,
342+
time=participant_time_str,
343+
timezone=participant_tz_abbr,
344+
first_name=participant.first_name,
345+
scheduled_calls_url="http://localhost:3000/participant/dashboard",
346+
language=participant_language,
347+
)
348+
349+
# Send to volunteer
350+
if volunteer.email:
351+
ses_service.send_call_scheduled_email(
352+
to_email=volunteer.email,
353+
match_name=f"{participant.first_name} {participant.last_name}"
354+
if participant.first_name and participant.last_name
355+
else "Your participant",
356+
date=volunteer_date,
357+
time=volunteer_time_str,
358+
timezone=volunteer_tz_abbr,
359+
first_name=volunteer.first_name,
360+
scheduled_calls_url="http://localhost:3000/volunteer/dashboard",
361+
language=volunteer_language,
362+
)
363+
364+
except Exception as e:
365+
# Log error but don't fail the scheduling
366+
self.logger.error(f"Failed to send call scheduled emails for match {match_id}: {e}")
367+
261368
return self._build_match_detail(match)
262369

263370
except HTTPException:
@@ -317,6 +424,35 @@ async def request_new_times(
317424
self.db.commit()
318425
self.db.refresh(match)
319426

427+
# Send "participant requested new times" email to volunteer
428+
try:
429+
# Load participant and volunteer
430+
participant = self.db.get(User, match.participant_id)
431+
volunteer = self.db.get(User, match.volunteer_id)
432+
433+
if participant and volunteer and volunteer.email:
434+
# Get volunteer's language
435+
volunteer_language = volunteer.language.value if volunteer.language else "en"
436+
437+
# Get participant's name for email
438+
participant_name = (
439+
f"{participant.first_name} {participant.last_name}"
440+
if participant.first_name and participant.last_name
441+
else "A participant"
442+
)
443+
444+
ses_service = SESEmailService()
445+
ses_service.send_participant_requested_new_times_email(
446+
to_email=volunteer.email,
447+
participant_name=participant_name,
448+
first_name=volunteer.first_name,
449+
matches_url="http://localhost:3000/volunteer/dashboard",
450+
language=volunteer_language,
451+
)
452+
except Exception as e:
453+
# Log error but don't fail the request
454+
self.logger.error(f"Failed to send participant requested new times email for match {match_id}: {e}")
455+
320456
return self._build_match_detail(match)
321457

322458
except HTTPException:
@@ -530,6 +666,27 @@ async def volunteer_accept_match(
530666
self.db.commit()
531667
self.db.refresh(match)
532668

669+
# Send "matches available" email to participant
670+
try:
671+
participant = match.participant
672+
if participant and participant.email:
673+
# Get participant's language (enum values are already "en" or "fr")
674+
language = participant.language.value if participant.language else "en"
675+
676+
first_name = participant.first_name if participant.first_name else None
677+
matches_url = "http://localhost:3000/participant/dashboard"
678+
679+
ses_service = SESEmailService()
680+
ses_service.send_matches_available_email(
681+
to_email=participant.email,
682+
first_name=first_name,
683+
matches_url=matches_url,
684+
language=language,
685+
)
686+
except Exception as e:
687+
# Log error but don't fail the match acceptance
688+
self.logger.error(f"Failed to send matches available email to participant {match.participant_id}: {e}")
689+
533690
# Return match detail for participant view (includes suggested times)
534691
return self._build_match_detail(match)
535692
except HTTPException:

backend/app/utilities/__init__.py

Whitespace-only changes.

backend/app/utilities/ses/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)