diff --git a/backend/app/routes/intake.py b/backend/app/routes/intake.py index 0f1513e5..795ccfa5 100644 --- a/backend/app/routes/intake.py +++ b/backend/app/routes/intake.py @@ -13,6 +13,7 @@ from app.schemas.user import UserRole from app.services.implementations.form_processor import FormProcessor from app.utilities.db_utils import get_db +from app.utilities.ses_email_service import SESEmailService # ===== Schemas ===== @@ -269,6 +270,21 @@ async def create_form_submission( db.commit() db.refresh(db_submission) + # Send intake form confirmation email for intake forms + if form and form.type == "intake": + try: + ses_service = SESEmailService() + # Get language (enum values are already "en" or "fr") + language = target_user.language.value if target_user.language else "en" + + first_name = target_user.first_name if target_user.first_name else None + ses_service.send_intake_form_confirmation_email( + to_email=target_user.email, first_name=first_name, language=language + ) + except Exception as e: + # Log error but don't fail the request + print(f"Failed to send intake form confirmation email: {str(e)}") + # Build response dict response_dict = { "id": db_submission.id, diff --git a/backend/app/services/implementations/auth_service.py b/backend/app/services/implementations/auth_service.py index 785ebddf..809483a8 100644 --- a/backend/app/services/implementations/auth_service.py +++ b/backend/app/services/implementations/auth_service.py @@ -49,6 +49,34 @@ def renew_token(self, refresh_token: str) -> Token: def reset_password(self, email: str) -> None: try: + # Get user's first name and language if available + first_name = None + language = "en" # Default to English + try: + # Try database first, then fall back to Firebase + try: + user = self.user_service.get_user_by_email(email) + if user: + if user.first_name and user.first_name.strip(): + first_name = user.first_name.strip() + # Get language from user (enum values are already "en" or "fr") + if user.language: + language = user.language.value + except Exception: + pass + + # Fall back to Firebase if database didn't have first_name + if not first_name: + try: + firebase_user = firebase_admin.auth.get_user_by_email(email) + if firebase_user and firebase_user.display_name: + display_name = firebase_user.display_name.strip() + first_name = display_name.split()[0] if display_name else None + except Exception: + pass + except Exception: + pass + # Use Firebase Admin SDK to generate password reset link action_code_settings = firebase_admin.auth.ActionCodeSettings( url="http://localhost:3000/set-new-password", @@ -57,8 +85,8 @@ def reset_password(self, email: str) -> None: reset_link = firebase_admin.auth.generate_password_reset_link(email, action_code_settings) - # Send via SES - email_sent = self.ses_email_service.send_password_reset_email(email, reset_link) + # Send via SES with language + email_sent = self.ses_email_service.send_password_reset_email(email, reset_link, first_name, language) if email_sent: self.logger.info(f"Password reset email sent successfully to {email}") diff --git a/backend/app/services/implementations/match_service.py b/backend/app/services/implementations/match_service.py index 20814385..1cad936b 100644 --- a/backend/app/services/implementations/match_service.py +++ b/backend/app/services/implementations/match_service.py @@ -24,6 +24,7 @@ ) from app.schemas.time_block import TimeBlockEntity, TimeRange from app.schemas.user import UserRole +from app.utilities.ses_email_service import SESEmailService from app.utilities.timezone_utils import get_timezone_from_abbreviation SCHEDULE_CLEANUP_STATUSES = { @@ -95,6 +96,28 @@ async def create_matches(self, req: MatchCreateRequest) -> MatchCreateResponse: for match in created_matches: self.db.refresh(match) + # Send "matches available" email to each volunteer + ses_service = SESEmailService() + for match in created_matches: + try: + volunteer = self.db.get(User, match.volunteer_id) + if volunteer and volunteer.email: + # Get volunteer's language (enum values are already "en" or "fr") + language = volunteer.language.value if volunteer.language else "en" + + first_name = volunteer.first_name if volunteer.first_name else None + matches_url = "http://localhost:3000/volunteer/dashboard" + + ses_service.send_matches_available_email( + to_email=volunteer.email, + first_name=first_name, + matches_url=matches_url, + language=language, + ) + except Exception as e: + # Log error but don't fail the match creation + self.logger.error(f"Failed to send matches available email to volunteer {match.volunteer_id}: {e}") + responses = [self._build_match_response(match) for match in created_matches] return MatchCreateResponse(matches=responses) @@ -258,6 +281,90 @@ async def schedule_match( self.db.commit() self.db.refresh(match) + # Send "call scheduled" email to both participant and volunteer + try: + # Load participant and volunteer with their data + participant = ( + self.db.query(User) + .options(joinedload(User.user_data)) + .filter(User.id == match.participant_id) + .first() + ) + volunteer = ( + self.db.query(User) + .options(joinedload(User.user_data)) + .filter(User.id == match.volunteer_id) + .first() + ) + + if participant and volunteer and match.confirmed_time: + ses_service = SESEmailService() + confirmed_time_utc = match.confirmed_time.start_time + + # Get participant's timezone and language + participant_tz = ZoneInfo("America/Toronto") # Default to EST + if participant.user_data and participant.user_data.timezone: + tz_result = get_timezone_from_abbreviation(participant.user_data.timezone) + if tz_result: + participant_tz = tz_result + + participant_language = participant.language.value if participant.language else "en" + + # Get volunteer's timezone and language + volunteer_tz = ZoneInfo("America/Toronto") # Default to EST + if volunteer.user_data and volunteer.user_data.timezone: + tz_result = get_timezone_from_abbreviation(volunteer.user_data.timezone) + if tz_result: + volunteer_tz = tz_result + + volunteer_language = volunteer.language.value if volunteer.language else "en" + + # Convert time to participant's timezone + participant_time = confirmed_time_utc.astimezone(participant_tz) + participant_date = participant_time.strftime("%B %d, %Y") + participant_time_str = participant_time.strftime("%I:%M %p") + participant_tz_abbr = participant_time.strftime("%Z") + + # Convert time to volunteer's timezone + volunteer_time = confirmed_time_utc.astimezone(volunteer_tz) + volunteer_date = volunteer_time.strftime("%B %d, %Y") + volunteer_time_str = volunteer_time.strftime("%I:%M %p") + volunteer_tz_abbr = volunteer_time.strftime("%Z") + + # Send to participant + if participant.email: + ses_service.send_call_scheduled_email( + to_email=participant.email, + match_name=f"{volunteer.first_name} {volunteer.last_name}" + if volunteer.first_name and volunteer.last_name + else "Your volunteer", + date=participant_date, + time=participant_time_str, + timezone=participant_tz_abbr, + first_name=participant.first_name, + scheduled_calls_url="http://localhost:3000/participant/dashboard", + language=participant_language, + ) + + # Send to volunteer + if volunteer.email: + ses_service.send_call_scheduled_email( + to_email=volunteer.email, + match_name=f"{participant.first_name} {participant.last_name}" + if participant.first_name and participant.last_name + else "Your participant", + date=volunteer_date, + time=volunteer_time_str, + timezone=volunteer_tz_abbr, + first_name=volunteer.first_name, + scheduled_calls_url="http://localhost:3000/volunteer/dashboard", + language=volunteer_language, + ) + + except Exception as e: + # Log error but don't fail the scheduling + self.logger.error(f"Failed to send call scheduled emails for match {match_id}: {e}") + return self._build_match_detail(match) except HTTPException: @@ -317,6 +424,35 @@ async def request_new_times( self.db.commit() self.db.refresh(match) + # Send "participant requested new times" email to volunteer + try: + # Load participant and volunteer + participant = self.db.get(User, match.participant_id) + volunteer = self.db.get(User, match.volunteer_id) + + if participant and volunteer and volunteer.email: + # Get volunteer's language + volunteer_language = volunteer.language.value if volunteer.language else "en" + + # Get participant's name for email + participant_name = ( + f"{participant.first_name} {participant.last_name}" + if participant.first_name and participant.last_name + else "A participant" + ) + + ses_service = SESEmailService() + ses_service.send_participant_requested_new_times_email( + to_email=volunteer.email, + participant_name=participant_name, + first_name=volunteer.first_name, + matches_url="http://localhost:3000/volunteer/dashboard", + language=volunteer_language, + ) + except Exception as e: + # Log error but don't fail the request + self.logger.error(f"Failed to send participant requested new times email for match {match_id}: {e}") + return self._build_match_detail(match) except HTTPException: @@ -530,6 +666,27 @@ async def volunteer_accept_match( self.db.commit() self.db.refresh(match) + # Send "matches available" email to participant + try: + participant = match.participant + if participant and participant.email: + # Get participant's language (enum values are already "en" or "fr") + language = participant.language.value if participant.language else "en" + + first_name = participant.first_name if participant.first_name else None + matches_url = "http://localhost:3000/participant/dashboard" + + ses_service = SESEmailService() + ses_service.send_matches_available_email( + to_email=participant.email, + first_name=first_name, + matches_url=matches_url, + language=language, + ) + except Exception as e: + # Log error but don't fail the match acceptance + self.logger.error(f"Failed to send matches available email to participant {match.participant_id}: {e}") + # Return match detail for participant view (includes suggested times) return self._build_match_detail(match) except HTTPException: diff --git a/backend/app/utilities/__init__.py b/backend/app/utilities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/utilities/ses/__init__.py b/backend/app/utilities/ses/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/utilities/ses/ses_templates.json b/backend/app/utilities/ses/ses_templates.json index ccbe85e5..cb2d2b66 100644 --- a/backend/app/utilities/ses/ses_templates.json +++ b/backend/app/utilities/ses/ses_templates.json @@ -1,26 +1,92 @@ [ { - "HtmlPart": "app/utilities/ses/template_files/test.html", + "HtmlPart": "app/utilities/ses/template_files/compiled/test.html", "SubjectPart": "Testing Email SES Template", "TemplateName": "Test", - "TextPart": "app/utilities/ses/template_files/test.txt" + "TextPart": "app/utilities/ses/template_files/text/test.txt" }, { - "HtmlPart": "app/utilities/ses/template_files/email_verification_en.html", - "SubjectPart": "Verify Your Email Address", + "HtmlPart": "app/utilities/ses/template_files/compiled/email_verification_en.html", + "SubjectPart": "{{first_name}}, confirm your email - First Connection Peer Support Program", "TemplateName": "EmailVerificationEn", - "TextPart": "app/utilities/ses/template_files/email_verification_en.txt" + "TextPart": "app/utilities/ses/template_files/text/email_verification_en.txt" }, { - "HtmlPart": "app/utilities/ses/template_files/email_verification_fr.html", - "SubjectPart": "Vérifiez votre adresse courriel", + "HtmlPart": "app/utilities/ses/template_files/compiled/email_verification_fr.html", + "SubjectPart": "{{first_name}}, confirmation de l'adresse courriel – Programme de soutien par les pairs Premier contact", "TemplateName": "EmailVerificationFr", - "TextPart": "app/utilities/ses/template_files/email_verification_fr.txt" + "TextPart": "app/utilities/ses/template_files/text/email_verification_fr.txt" }, { - "HtmlPart": "app/utilities/ses/template_files/password_reset.html", - "SubjectPart": "Reset Your Password", - "TemplateName": "PasswordReset", - "TextPart": "app/utilities/ses/template_files/password_reset.txt" + "HtmlPart": "app/utilities/ses/template_files/compiled/password_reset_en.html", + "SubjectPart": "Reset Your Password - First Connection Peer Support Program", + "TemplateName": "PasswordResetEn", + "TextPart": "app/utilities/ses/template_files/text/password_reset_en.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/password_reset_fr.html", + "SubjectPart": "Réinitialisation du mot de passe – Programme de soutien par les pairs Premier contact", + "TemplateName": "PasswordResetFr", + "TextPart": "app/utilities/ses/template_files/text/password_reset_fr.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/intake_form_confirmation_en.html", + "SubjectPart": "We received your intake form - First Connection Peer Support Program", + "TemplateName": "IntakeFormConfirmationEn", + "TextPart": "app/utilities/ses/template_files/text/intake_form_confirmation_en.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/intake_form_confirmation_fr.html", + "SubjectPart": "Réception de votre formulaire de demande – Programme de soutien par les pairs Premier contact", + "TemplateName": "IntakeFormConfirmationFr", + "TextPart": "app/utilities/ses/template_files/text/intake_form_confirmation_fr.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/matches_available_en.html", + "SubjectPart": "{{first_name}}, you have new matches - First Connection Peer Support Program", + "TemplateName": "MatchesAvailableEn", + "TextPart": "app/utilities/ses/template_files/text/matches_available_en.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/matches_available_fr.html", + "SubjectPart": "{{first_name}}, nouveaux jumelages – Programme de soutien par les pairs Premier contact", + "TemplateName": "MatchesAvailableFr", + "TextPart": "app/utilities/ses/template_files/text/matches_available_fr.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/call_scheduled_en.html", + "SubjectPart": "Call confirmed with {{match_name}} @ {{date}} {{time}} {{timezone}} - First Connection Peer Support Program", + "TemplateName": "CallScheduledEn", + "TextPart": "app/utilities/ses/template_files/text/call_scheduled_en.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/call_scheduled_fr.html", + "SubjectPart": "Confirmation de l'appel avec {{match_name}} le {{date}} à {{time}} {{timezone}} – Programme de soutien par les pairs Premier contact", + "TemplateName": "CallScheduledFr", + "TextPart": "app/utilities/ses/template_files/text/call_scheduled_fr.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/participant_requested_new_times_en.html", + "SubjectPart": "{{participant_name}} requested new times - First Connection Peer Support Program", + "TemplateName": "ParticipantRequestedNewTimesEn", + "TextPart": "app/utilities/ses/template_files/text/participant_requested_new_times_en.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/participant_requested_new_times_fr.html", + "SubjectPart": "Demande d'une autre plage horaire par {{participant_name}} – Programme de soutien par les pairs Premier contact", + "TemplateName": "ParticipantRequestedNewTimesFr", + "TextPart": "app/utilities/ses/template_files/text/participant_requested_new_times_fr.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/volunteer_accepted_new_times_en.html", + "SubjectPart": "{{volunteer_name}} confirmed your new time @ {{date}} {{time}} {{timezone}} - First Connection Peer Support Program", + "TemplateName": "VolunteerAcceptedNewTimesEn", + "TextPart": "app/utilities/ses/template_files/text/volunteer_accepted_new_times_en.txt" + }, + { + "HtmlPart": "app/utilities/ses/template_files/compiled/volunteer_accepted_new_times_fr.html", + "SubjectPart": "Confirmation de la nouvelle plage horaire, le {{date}} à {{time}} {{timezone}}, par {{volunteer_name}} – Programme de soutien par les pairs Premier contact", + "TemplateName": "VolunteerAcceptedNewTimesFr", + "TextPart": "app/utilities/ses/template_files/text/volunteer_accepted_new_times_fr.txt" } ] diff --git a/backend/app/utilities/ses/template_files/email_verification.html b/backend/app/utilities/ses/template_files/base/base_email_en.html similarity index 78% rename from backend/app/utilities/ses/template_files/email_verification.html rename to backend/app/utilities/ses/template_files/base/base_email_en.html index 4ebbd8ec..1983df03 100644 --- a/backend/app/utilities/ses/template_files/email_verification.html +++ b/backend/app/utilities/ses/template_files/base/base_email_en.html @@ -10,7 +10,7 @@
|
-
-
+ -
Hi {{first_name}}, -Click the link below to verify your email address: {{verification_link}} -If the link doesn't work, copy and paste it into your browser. -This link will expire in 24 hours. If you didn't request this verification, please ignore this email. + {% block content %} + + {% endblock %}@@ -73,10 +77,8 @@ border="0" width="25" height="25" - id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1044" src="http://secure.llscanada.org/images/content/pagebuilder/instagram-logo.png" alt="Instagram" - class="gmail-CToWUd" style="width: 0.2604in; height: 0.2604in;" > @@ -92,10 +94,8 @@ border="0" width="25" height="25" - id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1043" src="http://secure.llscanada.org/images/content/pagebuilder/facebook-logo.png" alt="Facebook" - class="gmail-CToWUd" style="width: 0.2604in; height: 0.2604in;" > @@ -111,10 +111,8 @@ border="0" width="25" height="25" - id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1042" src="http://secure.llscanada.org/images/content/pagebuilder/linkedin-logo.png" alt="LinkedIn" - class="gmail-CToWUd" style="width: 0.2604in; height: 0.2604in;" > @@ -130,10 +128,8 @@ border="0" width="25" height="25" - id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1041" src="http://secure.llscanada.org/images/content/pagebuilder/YouTube_logo.png" alt="YouTube" - class="gmail-CToWUd" style="width: 0.2604in; height: 0.2604in;" > @@ -149,10 +145,8 @@ border="0" width="25" height="25" - id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1040" src="http://secure.llscanada.org/images/content/pagebuilder/twitter-x.jpg" alt="X" - class="gmail-CToWUd" style="width: 0.2604in; height: 0.2604in;" > @@ -168,10 +162,8 @@ border="0" width="25" height="25" - id="m_4346998286330489325m_-277460127918842692m_5180834574350019448m_-2438363903363548788m_-1430917162020640728m_-2619392195472490027m_2564292851399425314m_817068749652786359_x0000_i1039" src="http://secure.llscanada.org/images/content/pagebuilder/spotify-logo.png" alt="Spotify" - class="gmail-CToWUd" style="width: 0.2604in; height: 0.2604in;" > @@ -211,7 +203,7 @@ |
|
+
+
+
+ REMARQUE : Veuillez ne pas répondre au présent courriel. Il est envoyé automatiquement à partir d'une boîte de messagerie non surveillée – aucune réponse ne sera reçue ou traitée. Pour toute question ou préoccupation concernant le programme de soutien par les pairs Premier contact, écrivez à
+ FirstConnection@bloodcancers.ca.
+
+
|
+
|
+
+
+
+ NOTICE: Please do not reply to this email. It is automated from an unmonitored email address and will not be received or responded to. Please contact
+ FirstConnection@bloodcancers.ca for any questions or concerns related to the First Connection Peer Support Program.
+
+
|
+
|
+
+
+
+ REMARQUE : Veuillez ne pas répondre au présent courriel. Il est envoyé automatiquement à partir d'une boîte de messagerie non surveillée – aucune réponse ne sera reçue ou traitée. Pour toute question ou préoccupation concernant le programme de soutien par les pairs Premier contact, écrivez à
+ FirstConnection@bloodcancers.ca.
+
+
|
+
Hi {{first_name}},
-Click the link below to verify your email address: {{verification_link}}
-If the link doesn't work, copy and paste it into your browser.
-This link will expire in 24 hours. If you didn't request this verification, please ignore this email.
+ +Hi {{first_name}},
+Click the link to verify your email: {{verification_link}}
+If the link doesn't work, copy and paste it into your browser.
+This link will expire in 24 hours. If you didn't create an account, please ignore this email.
+