From 051b479e76ebcbf20592a81e347ee7b6ffe6ff86 Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:29:25 -0400 Subject: [PATCH 1/8] feat: Rename mail.send_* functions to match a MessageType. Use new send() instead of mail.send_* and Message.create(). --- app/__main__.py | 45 ++----------- app/mail.py | 102 +++++++++++++++++------------- app/routers/applications.py | 46 ++++---------- app/routers/guest/applications.py | 47 ++------------ app/routers/guest/emails.py | 13 ++-- app/routers/users.py | 6 +- 6 files changed, 94 insertions(+), 165 deletions(-) diff --git a/app/__main__.py b/app/__main__.py index 98b8519c..7b1e5a19 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -90,13 +90,7 @@ def _create_complete_application( expired_at=datetime.utcnow() + timedelta(days=app_settings.application_expiration_days), ) - message_id = mail.send_invitation_email(aws.ses_client, application) - models.Message.create( - session, - application=application, - type=models.MessageType.BORROWER_INVITATION, - external_message_id=message_id, - ) + mail.send(session, aws.ses_client, models.MessageType.BORROWER_INVITATION, application) session.commit() @@ -184,13 +178,7 @@ def send_reminders() -> None: if not state["quiet"]: print(f"Sending {len(pending_introduction_reminder)} BORROWER_PENDING_APPLICATION_REMINDER...") for application in pending_introduction_reminder: - message_id = mail.send_mail_intro_reminder(aws.ses_client, application) - models.Message.create( - session, - application=application, - type=models.MessageType.BORROWER_PENDING_APPLICATION_REMINDER, - external_message_id=message_id, - ) + mail.send(session, aws.ses_client, models.MessageType.BORROWER_PENDING_APPLICATION_REMINDER, application) session.commit() @@ -205,13 +193,7 @@ def send_reminders() -> None: if not state["quiet"]: print(f"Sending {len(pending_submission_reminder)} BORROWER_PENDING_SUBMIT_REMINDER...") for application in pending_submission_reminder: - message_id = mail.send_mail_submit_reminder(aws.ses_client, application) - models.Message.create( - session, - application=application, - type=models.MessageType.BORROWER_PENDING_SUBMIT_REMINDER, - external_message_id=message_id, - ) + mail.send(session, aws.ses_client, models.MessageType.BORROWER_PENDING_SUBMIT_REMINDER, application) session.commit() @@ -256,28 +238,15 @@ def sla_overdue_applications() -> None: if days_passed > application.lender.sla_days: application.overdued_at = datetime.now(application.created_at.tzinfo) - message_id = mail.send_overdue_application_email_to_ocp(aws.ses_client, application) - models.Message.create( - session, - application=application, - type=models.MessageType.OVERDUE_APPLICATION, - external_message_id=message_id, - ) + mail.send(session, aws.ses_client, models.MessageType.OVERDUE_APPLICATION, application) session.commit() for lender_id, lender_data in overdue_lenders.items(): - message_id = mail.send_overdue_application_email_to_lender( + mail.send_overdue_application_to_lender( aws.ses_client, - models.Lender.get(session, id=lender_id), - lender_data["count"], - ) - models.Message.create( - session, - # NOTE: A random application that might not even be to the lender, but application is not nullable. - application=application, - type=models.MessageType.OVERDUE_APPLICATION, - external_message_id=message_id, + lender=models.Lender.get(session, id=lender_id), + amount=lender_data["count"], ) session.commit() diff --git a/app/mail.py b/app/mail.py index c1a2a462..8dea3b5d 100644 --- a/app/mail.py +++ b/app/mail.py @@ -1,14 +1,16 @@ import json import logging import os +import sys from pathlib import Path from typing import Any from urllib.parse import quote from mypy_boto3_ses.client import SESClient +from sqlalchemy.orm import Session from app.i18n import _ -from app.models import Application, Lender, MessageType +from app.models import Application, Lender, Message, MessageType from app.settings import app_settings logger = logging.getLogger(__name__) @@ -41,6 +43,23 @@ def get_template_data(template_name: str, subject: str, parameters: dict[str, An } +def send( + session: Session, + ses: SESClient, + message_type: str, + application: Application | None, + *, + message_kwargs: dict[str, Any] | None = None, + **send_kwargs: Any, +) -> None: + message_id = getattr(sys.modules[__name__], f"send_{message_type.lower()}")(ses, application, **send_kwargs) + if message_kwargs is None: + message_kwargs = {} + Message.create( + session, application=application, type=message_type, external_message_id=message_id, **message_kwargs + ) + + def send_email(ses: SESClient, emails: list[str], data: dict[str, str], *, to_borrower: bool = True) -> str: if app_settings.environment == "production" or not to_borrower: to_addresses = emails @@ -63,7 +82,7 @@ def get_lender_emails(lender: Lender, message_type: MessageType): return [user.email for user in lender.users if user.notification_preferences.get(message_type)] -def send_application_approved_email(ses: SESClient, application: Application) -> str: +def send_approved_application(ses: SESClient, application: Application) -> str: """ Sends an email notification when an application has been approved. @@ -96,7 +115,7 @@ def send_application_approved_email(ses: SESClient, application: Application) -> ) -def send_application_submission_completed(ses: SESClient, application: Application) -> str: +def send_submission_completed(ses: SESClient, application: Application) -> str: """ Sends an email notification when an application is submitted. @@ -117,7 +136,7 @@ def send_application_submission_completed(ses: SESClient, application: Applicati ) -def send_application_credit_disbursed(ses: SESClient, application: Application) -> str: +def send_credit_disbursed(ses: SESClient, application: Application) -> str: """ Sends an email notification when an application has the credit dibursed. @@ -139,7 +158,7 @@ def send_application_credit_disbursed(ses: SESClient, application: Application) ) -def send_mail_to_new_user(ses: SESClient, name: str, username: str, temporary_password: str) -> str: +def send_new_user(ses: SESClient, *, name: str, username: str, temporary_password: str) -> str: """ Sends an email to a new user with a link to set their password. @@ -169,7 +188,7 @@ def send_mail_to_new_user(ses: SESClient, name: str, username: str, temporary_pa ) -def send_upload_contract_notification_to_lender(ses: SESClient, application: Application) -> str: +def send_contract_upload_confirmation_to_fi(ses: SESClient, application: Application) -> str: """ Sends an email to the lender to notify them of a new contract submission. @@ -191,7 +210,7 @@ def send_upload_contract_notification_to_lender(ses: SESClient, application: App ) -def send_upload_contract_confirmation(ses: SESClient, application: Application) -> str: +def send_contract_upload_confirmation(ses: SESClient, application: Application) -> str: """ Sends an email to the borrower confirming the successful upload of the contract. @@ -213,8 +232,8 @@ def send_upload_contract_confirmation(ses: SESClient, application: Application) ) -def send_new_email_confirmation( - ses: SESClient, application: Application, new_email: str, confirmation_email_token: str +def send_email_change_confirmation( + ses: SESClient, application: Application, *, new_email: str, confirmation_email_token: str ) -> str: """ Sends an email to confirm the new primary email for the borrower. @@ -242,7 +261,7 @@ def send_new_email_confirmation( return send_email(ses, [new_email], data) -def send_mail_to_reset_password(ses: SESClient, username: str, temporary_password: str) -> str: +def send_reset_password(ses: SESClient, *, username: str, temporary_password: str) -> str: """ Sends an email to a user with instructions to reset their password. @@ -282,7 +301,7 @@ def get_invitation_email_parameters(application: Application) -> dict[str, str]: } -def send_invitation_email(ses: SESClient, application: Application) -> str: +def send_borrower_invitation(ses: SESClient, application: Application) -> str: """ Sends an invitation email to the provided email address. @@ -300,7 +319,7 @@ def send_invitation_email(ses: SESClient, application: Application) -> str: ) -def send_mail_intro_reminder(ses: SESClient, application: Application) -> str: +def send_borrower_pending_application_reminder(ses: SESClient, application: Application) -> str: """ Sends an introductory reminder email to the provided email address. @@ -318,7 +337,7 @@ def send_mail_intro_reminder(ses: SESClient, application: Application) -> str: ) -def send_mail_submit_reminder(ses: SESClient, application: Application) -> str: +def send_borrower_pending_submit_reminder(ses: SESClient, application: Application) -> str: """ Sends a submission reminder email to the provided email address. @@ -342,7 +361,7 @@ def send_mail_submit_reminder(ses: SESClient, application: Application) -> str: ) -def send_notification_new_app_to_lender(ses: SESClient, lender: Lender) -> str: +def send_new_application_fi(ses: SESClient, lender: Lender) -> str: """ Sends a notification email about a new application to a lender's email group. @@ -362,7 +381,7 @@ def send_notification_new_app_to_lender(ses: SESClient, lender: Lender) -> str: ) -def send_notification_new_app_to_ocp(ses: SESClient, application: Application) -> str: +def send_new_application_ocp(ses: SESClient, application: Application) -> str: """ Sends a notification email about a new application to the Open Contracting Partnership's (OCP) email group. """ @@ -381,11 +400,11 @@ def send_notification_new_app_to_ocp(ses: SESClient, application: Application) - ) -def send_mail_request_to_borrower(ses: SESClient, application: Application, email_message: str) -> str: +def send_fi_message(ses: SESClient, application: Application, *, message: str) -> str: """ Sends an email request to the borrower for additional data. - :param email_message: Message content from the lender to be included in the email. + :param message: Message content from the lender to be included in the email. """ return send_email( ses, @@ -395,14 +414,14 @@ def send_mail_request_to_borrower(ses: SESClient, application: Application, emai _("New message from a financial institution"), { "LENDER_NAME": application.lender.name, - "LENDER_MESSAGE": email_message, + "LENDER_MESSAGE": message, "LOGIN_DOCUMENTS_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/documents", }, ), ) -def send_overdue_application_email_to_lender(ses: SESClient, lender: Lender, amount: int) -> str: +def send_overdue_application_to_lender(ses: SESClient, *, lender: Lender, amount: int) -> str: """ Sends an email notification to the lender about overdue applications. @@ -425,7 +444,7 @@ def send_overdue_application_email_to_lender(ses: SESClient, lender: Lender, amo ) -def send_overdue_application_email_to_ocp(ses: SESClient, application: Application) -> str: +def send_overdue_application(ses: SESClient, application: Application) -> str: """ Sends an email notification to the Open Contracting Partnership (OCP) about overdue applications. """ @@ -445,32 +464,27 @@ def send_overdue_application_email_to_ocp(ses: SESClient, application: Applicati ) -def send_rejected_application_email(ses: SESClient, application: Application) -> str: +def send_rejected_application(ses: SESClient, application: Application, *, options: bool) -> str: """ Sends an email notification to the applicant when an application has been rejected. """ - return send_email( - ses, - [application.primary_email], - get_template_data( - "Application_declined", - _("Your credit application has been declined"), - { - "LENDER_NAME": application.lender.name, - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - "FIND_ALTENATIVE_URL": ( - f"{app_settings.frontend_url}/application/{quote(application.uuid)}/find-alternative-credit" - ), - }, - ), - ) - + if options: + return send_email( + ses, + application.primary_email, + get_template_data( + "Application_declined", + _("Your credit application has been declined"), + { + "LENDER_NAME": application.lender.name, + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + "FIND_ALTENATIVE_URL": ( + f"{app_settings.frontend_url}/application/{quote(application.uuid)}/find-alternative-credit" + ), + }, + ), + ) -def send_rejected_application_email_without_alternatives(ses: SESClient, application: Application) -> str: - """ - Sends an email notification to the applicant when an application has been rejected, - and no alternatives are available. - """ return send_email( ses, [application.primary_email], @@ -485,7 +499,7 @@ def send_rejected_application_email_without_alternatives(ses: SESClient, applica ) -def send_copied_application_notification_to_borrower(ses: SESClient, application: Application) -> str: +def send_application_copied(ses: SESClient, application: Application) -> str: """ Sends an email notification to the borrower when an application has been copied, allowing them to continue with the application process. @@ -504,7 +518,7 @@ def send_copied_application_notification_to_borrower(ses: SESClient, application ) -def send_upload_documents_notifications_to_lender(ses: SESClient, application: Application) -> str: +def send_borrower_document_updated(ses: SESClient, application: Application) -> str: """ Sends an email notification to the lender to notify them that new documents have been uploaded and are ready for their review. diff --git a/app/routers/applications.py b/app/routers/applications.py index 6d6b405f..dd1c91e8 100644 --- a/app/routers/applications.py +++ b/app/routers/applications.py @@ -45,7 +45,7 @@ async def reject_application( payload_dict = jsonable_encoder(payload, exclude_unset=True) application.stage_as_rejected(payload_dict) - options = ( + options = session.query( session.query(models.CreditProduct) .join(models.Lender) .options(joinedload(models.CreditProduct.lender)) @@ -55,8 +55,8 @@ async def reject_application( col(models.CreditProduct.lower_limit) <= application.amount_requested, col(models.CreditProduct.upper_limit) >= application.amount_requested, ) - .all() - ) + .exists() + ).scalar() models.ApplicationAction.create( session, @@ -66,16 +66,7 @@ async def reject_application( user_id=user.id, ) - if options: - message_id = mail.send_rejected_application_email(client.ses, application) - else: - message_id = mail.send_rejected_application_email_without_alternatives(client.ses, application) - models.Message.create( - session, - application=application, - type=models.MessageType.REJECTED_APPLICATION, - external_message_id=message_id, - ) + mail.send(session, client.ses, models.MessageType.REJECTED_APPLICATION, application, options=options) session.commit() return application @@ -118,13 +109,7 @@ async def complete_application( user_id=user.id, ) - message_id = mail.send_application_credit_disbursed(client.ses, application) - models.Message.create( - session, - application=application, - type=models.MessageType.CREDIT_DISBURSED, - external_message_id=message_id, - ) + mail.send(session, client.ses, models.MessageType.CREDIT_DISBURSED, application) session.commit() return application @@ -196,13 +181,7 @@ async def approve_application( user_id=user.id, ) - message_id = mail.send_application_approved_email(client.ses, application) - models.Message.create( - session, - application=application, - type=models.MessageType.APPROVED_APPLICATION, - external_message_id=message_id, - ) + mail.send(session, client.ses, models.MessageType.APPROVED_APPLICATION, application) session.commit() return application @@ -551,14 +530,13 @@ async def email_borrower( user_id=user.id, ) - message_id = mail.send_mail_request_to_borrower(client.ses, application, payload.message) - models.Message.create( + mail.send( session, - application_id=application.id, - body=payload.message, - lender_id=application.lender.id, - type=models.MessageType.FI_MESSAGE, - external_message_id=message_id, + client.ses, + models.MessageType.FI_MESSAGE, + application, + message=payload.message, + message_kwargs={"body": payload.message, "lender_id": application.lender.id}, ) session.commit() diff --git a/app/routers/guest/applications.py b/app/routers/guest/applications.py index d783f089..6cb82969 100644 --- a/app/routers/guest/applications.py +++ b/app/routers/guest/applications.py @@ -532,16 +532,9 @@ async def update_apps_send_notifications( application.borrower_submitted_at = datetime.now(application.created_at.tzinfo) application.pending_documents = False - mail.send_notification_new_app_to_lender(client.ses, application.lender) - mail.send_notification_new_app_to_ocp(client.ses, application) - - message_id = mail.send_application_submission_completed(client.ses, application) - models.Message.create( - session, - application=application, - type=models.MessageType.SUBMISSION_COMPLETED, - external_message_id=message_id, - ) + mail.send_new_application_ocp(client.ses, application) + mail.send_new_application_fi(client.ses, application) + mail.send(session, client.ses, models.MessageType.SUBMISSION_COMPLETED, application) session.commit() return serializers.ApplicationResponse( @@ -637,13 +630,7 @@ async def complete_information_request( application_id=application.id, ) - message_id = mail.send_upload_documents_notifications_to_lender(client.ses, application) - models.Message.create( - session, - application=application, - type=models.MessageType.BORROWER_DOCUMENT_UPDATED, - external_message_id=message_id, - ) + mail.send(session, client.ses, models.MessageType.BORROWER_DOCUMENT_UPDATED, application) session.commit() return serializers.ApplicationResponse( @@ -723,22 +710,8 @@ async def confirm_upload_contract( application_id=application.id, ) - lender_message_id, borrower_message_id = ( - mail.send_upload_contract_notification_to_lender(client.ses, application), - mail.send_upload_contract_confirmation(client.ses, application), - ) - models.Message.create( - session, - application=application, - type=models.MessageType.CONTRACT_UPLOAD_CONFIRMATION_TO_FI, - external_message_id=lender_message_id, - ) - models.Message.create( - session, - application=application, - type=models.MessageType.CONTRACT_UPLOAD_CONFIRMATION, - external_message_id=borrower_message_id, - ) + mail.send(session, client.ses, models.MessageType.CONTRACT_UPLOAD_CONFIRMATION_TO_FI, application) + mail.send(session, client.ses, models.MessageType.CONTRACT_UPLOAD_CONFIRMATION, application) session.commit() return serializers.ApplicationResponse( @@ -819,13 +792,7 @@ async def find_alternative_credit_option( application_id=new_application.id, ) - message_id = mail.send_copied_application_notification_to_borrower(client.ses, new_application) - models.Message.create( - session, - application=new_application, - type=models.MessageType.APPLICATION_COPIED, - external_message_id=message_id, - ) + mail.send(session, client.ses, models.MessageType.APPLICATION_COPIED, new_application) session.commit() return serializers.ApplicationResponse( diff --git a/app/routers/guest/emails.py b/app/routers/guest/emails.py index aba55140..3cc9018b 100644 --- a/app/routers/guest/emails.py +++ b/app/routers/guest/emails.py @@ -45,14 +45,13 @@ async def change_email( application_id=application.id, ) - message_id = mail.send_new_email_confirmation( - client.ses, application, payload.new_email, confirmation_email_token - ) - models.Message.create( + mail.send( session, - application=application, - type=models.MessageType.EMAIL_CHANGE_CONFIRMATION, - external_message_id=message_id, + client.ses, + models.MessageType.EMAIL_CHANGE_CONFIRMATION, + application, + new_email=payload.new_email, + confirmation_email_token=confirmation_email_token, ) session.commit() diff --git a/app/routers/users.py b/app/routers/users.py index ab992536..4cf0e9c4 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -47,7 +47,9 @@ async def create_user( session.commit() - mail.send_mail_to_new_user(client.ses, payload.name, payload.email, temporary_password) + mail.send_new_user( + client.ses, name=payload.name, username=payload.email, temporary_password=temporary_password + ) return user except (client.cognito.exceptions.UsernameExistsException, IntegrityError): @@ -258,7 +260,7 @@ def forgot_password( Permanent=False, ) - mail.send_mail_to_reset_password(client.ses, payload.username, temporary_password) + mail.send_reset_password(client.ses, username=payload.username, temporary_password=temporary_password) # always return 200 to avoid user enumeration return serializers.ResponseBase(detail=_("An email with a reset link was sent to end user")) From f1b242b046e0fba901611db1573259b6d9cdc4e1 Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:01:29 -0400 Subject: [PATCH 2/8] chore(refactor): Merge functions into send(). Rename email templates to match message types. Make some functions private. --- app/mail.py | 559 ++++++------------ app/routers/guest/applications.py | 4 +- email_templates/__init__.py | 0 ...t_msme.html => application_copied.en.html} | 0 ...sme_es.html => application_copied.es.html} | 0 ...oved.html => approved_application.en.html} | 0 ...d_es.html => approved_application.es.html} | 0 ...html => borrower_document_updated.en.html} | 0 ...html => borrower_document_updated.es.html} | 0 ...MSMEs.html => borrower_invitation.en.html} | 0 ...Es_es.html => borrower_invitation.es.html} | 0 ...ower_pending_application_reminder.en.html} | 0 ...ower_pending_application_reminder.es.html} | 0 ... borrower_pending_submit_reminder.en.html} | 0 ... borrower_pending_submit_reminder.es.html} | 0 ...l => contract_upload_confirmation.en.html} | 0 ...l => contract_upload_confirmation.es.html} | 0 ...ontract_upload_confirmation_to_fi.en.html} | 0 ...ontract_upload_confirmation_to_fi.es.html} | 0 ...isbursed.html => credit_disbursed.en.html} | 0 ...ursed_es.html => credit_disbursed.es.html} | 0 ...html => email_change_confirmation.en.html} | 0 ...html => email_change_confirmation.es.html} | 0 ...st_data_to_SME.html => fi_message.en.html} | 0 ...data_to_SME_es.html => fi_message.es.html} | 0 ...I_user.html => new_application_fi.en.html} | 0 ...ser_es.html => new_application_fi.es.html} | 0 ..._user.html => new_application_ocp.en.html} | 0 ...er_es.html => new_application_ocp.es.html} | 0 ..._Account_Created.html => new_user.en.html} | 0 ...count_Created_es.html => new_user.es.html} | 0 ...admin.html => overdue_application.en.html} | 0 ...in_es.html => overdue_application.es.html} | 0 ... => overdue_application_to_lender.en.html} | 0 ... => overdue_application_to_lender.es.html} | 0 ...rejected_application_alternatives.en.html} | 0 ...rejected_application_alternatives.es.html} | 0 ...ected_application_no_alternatives.en.html} | 0 ...ected_application_no_alternatives.es.html} | 0 ...t_password.html => reset_password.en.html} | 0 ...assword_es.html => reset_password.es.html} | 0 ...tted.html => submission_completed.en.html} | 0 ...d_es.html => submission_completed.es.html} | 0 43 files changed, 182 insertions(+), 381 deletions(-) delete mode 100644 email_templates/__init__.py rename email_templates/{alternative_credit_msme.html => application_copied.en.html} (100%) rename email_templates/{alternative_credit_msme_es.html => application_copied.es.html} (100%) rename email_templates/{Application_approved.html => approved_application.en.html} (100%) rename email_templates/{Application_approved_es.html => approved_application.es.html} (100%) rename email_templates/{FI_Documents_Updated_FI_user.html => borrower_document_updated.en.html} (100%) rename email_templates/{FI_Documents_Updated_FI_user_es.html => borrower_document_updated.es.html} (100%) rename email_templates/{Access_to_credit_scheme_for_MSMEs.html => borrower_invitation.en.html} (100%) rename email_templates/{Access_to_credit_scheme_for_MSMEs_es.html => borrower_invitation.es.html} (100%) rename email_templates/{Access_to_credit_reminder.html => borrower_pending_application_reminder.en.html} (100%) rename email_templates/{Access_to_credit_reminder_es.html => borrower_pending_application_reminder.es.html} (100%) rename email_templates/{Complete_application_reminder.html => borrower_pending_submit_reminder.en.html} (100%) rename email_templates/{Complete_application_reminder_es.html => borrower_pending_submit_reminder.es.html} (100%) rename email_templates/{Contract_upload_confirmation.html => contract_upload_confirmation.en.html} (100%) rename email_templates/{Contract_upload_confirmation_es.html => contract_upload_confirmation.es.html} (100%) rename email_templates/{New_contract_submission.html => contract_upload_confirmation_to_fi.en.html} (100%) rename email_templates/{New_contract_submission_es.html => contract_upload_confirmation_to_fi.es.html} (100%) rename email_templates/{Application_credit_disbursed.html => credit_disbursed.en.html} (100%) rename email_templates/{Application_credit_disbursed_es.html => credit_disbursed.es.html} (100%) rename email_templates/{Confirm_email_address_change.html => email_change_confirmation.en.html} (100%) rename email_templates/{Confirm_email_address_change_es.html => email_change_confirmation.es.html} (100%) rename email_templates/{Request_data_to_SME.html => fi_message.en.html} (100%) rename email_templates/{Request_data_to_SME_es.html => fi_message.es.html} (100%) rename email_templates/{FI_New_application_submission_FI_user.html => new_application_fi.en.html} (100%) rename email_templates/{FI_New_application_submission_FI_user_es.html => new_application_fi.es.html} (100%) rename email_templates/{New_application_submission_OCP_user.html => new_application_ocp.en.html} (100%) rename email_templates/{New_application_submission_OCP_user_es.html => new_application_ocp.es.html} (100%) rename email_templates/{New_Account_Created.html => new_user.en.html} (100%) rename email_templates/{New_Account_Created_es.html => new_user.es.html} (100%) rename email_templates/{Overdue_application_OCP_admin.html => overdue_application.en.html} (100%) rename email_templates/{Overdue_application_OCP_admin_es.html => overdue_application.es.html} (100%) rename email_templates/{Overdue_application_FI.html => overdue_application_to_lender.en.html} (100%) rename email_templates/{Overdue_application_FI_es.html => overdue_application_to_lender.es.html} (100%) rename email_templates/{Application_declined.html => rejected_application_alternatives.en.html} (100%) rename email_templates/{Application_declined_es.html => rejected_application_alternatives.es.html} (100%) rename email_templates/{Application_declined_without_alternative.html => rejected_application_no_alternatives.en.html} (100%) rename email_templates/{Application_declined_without_alternative_es.html => rejected_application_no_alternatives.es.html} (100%) rename email_templates/{Reset_password.html => reset_password.en.html} (100%) rename email_templates/{Reset_password_es.html => reset_password.es.html} (100%) rename email_templates/{Application_submitted.html => submission_completed.en.html} (100%) rename email_templates/{Application_submitted_es.html => submission_completed.es.html} (100%) diff --git a/app/mail.py b/app/mail.py index cdf4695b..bbaff24f 100644 --- a/app/mail.py +++ b/app/mail.py @@ -1,7 +1,6 @@ import json import logging import os -import sys from pathlib import Path from typing import Any from urllib.parse import quote @@ -24,9 +23,7 @@ def get_template_data(template_name: str, subject: str, parameters: dict[str, An template, then return all tags required by the email template. """ with open( - os.path.join( - BASE_TEMPLATES_PATH, f"{template_name}{'_es' if app_settings.email_template_lang == 'es' else ''}.html" - ), + os.path.join(BASE_TEMPLATES_PATH, f"{template_name}.{app_settings.email_template_lang}.html"), encoding="utf-8", ) as f: html = f.read() @@ -47,17 +44,181 @@ def send( session: Session, ses: SESClient, message_type: str, - application: Application | None, + application: Application, *, + save: bool = True, message_kwargs: dict[str, Any] | None = None, **send_kwargs: Any, ) -> None: - message_id = getattr(sys.modules[__name__], f"send_{message_type.lower()}")(ses, application, **send_kwargs) - if message_kwargs is None: - message_kwargs = {} - Message.create( - session, application=application, type=message_type, external_message_id=message_id, **message_kwargs - ) + # The template name can be overridden by the match statement, if it is conditional on `send_kwargs`. + # If so, use new template names for each condition. + template_name = message_type.lower() + + # recipients is a list of lists. Each sublist is a `ToAddresses` parameter for an email message. + match message_type: + case MessageType.BORROWER_INVITATION: + recipients = [[application.primary_email]] + subject = _("Opportunity to access MSME credit for being awarded a public contract") + parameters = _get_borrower_invitation_parameters(application) + + case MessageType.BORROWER_PENDING_APPLICATION_REMINDER: + recipients = [[application.primary_email]] + subject = _("Opportunity to access MSME credit for being awarded a public contract") + parameters = _get_borrower_invitation_parameters(application) + + case MessageType.BORROWER_PENDING_SUBMIT_REMINDER: + recipients = [[application.primary_email]] + subject = _("Reminder - Opportunity to access MSME credit for being awarded a public contract") + parameters = { + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + "TENDER_TITLE": application.award.title, + "BUYER_NAME": application.award.buyer_name, + "APPLY_FOR_CREDIT_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/intro", + "REMOVE_ME_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/decline", + } + + case MessageType.SUBMISSION_COMPLETED: + recipients = [[application.primary_email]] + subject = _("Application Submission Complete") + parameters = { + "LENDER_NAME": application.lender.name, + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + } + + case MessageType.NEW_APPLICATION_OCP: + recipients = [[app_settings.ocp_email_group]] + subject = _("New application submission") + parameters = {"LOGIN_URL": f"{app_settings.frontend_url}/login", "LENDER_NAME": application.lender.name} + + case MessageType.NEW_APPLICATION_FI: + recipients = [_get_lender_emails(application.lender, MessageType.NEW_APPLICATION_FI)] + subject = _("New application submission") + parameters = {"LOGIN_URL": f"{app_settings.frontend_url}/login"} + + case MessageType.FI_MESSAGE: + recipients = [[application.primary_email]] + subject = _("New message from a financial institution") + parameters = { + "LENDER_NAME": application.lender.name, + "LENDER_MESSAGE": send_kwargs["message"], + "LOGIN_DOCUMENTS_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/documents", + } + + case MessageType.BORROWER_DOCUMENT_UPDATED: + recipients = [_get_lender_emails(application.lender, MessageType.BORROWER_DOCUMENT_UPDATED)] + subject = _("Application updated") + parameters = {"LOGIN_URL": f"{app_settings.frontend_url}/login"} + + case MessageType.REJECTED_APPLICATION: + recipients = [[application.primary_email]] + subject = _("Your credit application has been declined") + parameters = { + "LENDER_NAME": application.lender.name, + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + } + + if send_kwargs["options"]: + template_name = "rejected_application_alternatives" + parameters["FIND_ALTENATIVE_URL"] = ( + f"{app_settings.frontend_url}/application/{quote(application.uuid)}/find-alternative-credit" + ) + else: + template_name = "rejected_application_no_alternatives" + + case MessageType.APPROVED_APPLICATION: + if application.lender.default_pre_approval_message: + additional_comments = application.lender.default_pre_approval_message + elif application.lender_approved_data.get("additional_comments"): + additional_comments = application.lender_approved_data["additional_comments"] + else: + additional_comments = "Ninguno" + + recipients = [[application.primary_email]] + subject = _("Your credit application has been prequalified") + parameters = { + "LENDER_NAME": application.lender.name, + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + "TENDER_TITLE": application.award.title, + "BUYER_NAME": application.award.buyer_name, + "ADDITIONAL_COMMENTS": additional_comments, + "UPLOAD_CONTRACT_URL": ( + f"{app_settings.frontend_url}/application/{quote(application.uuid)}/upload-contract" + ), + } + + case MessageType.CONTRACT_UPLOAD_CONFIRMATION: + recipients = [[application.primary_email]] + subject = _("Thank you for uploading the signed contract") + parameters = { + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + "TENDER_TITLE": application.award.title, + "BUYER_NAME": application.award.buyer_name, + } + + case MessageType.CONTRACT_UPLOAD_CONFIRMATION_TO_FI: + recipients = [_get_lender_emails(application.lender, MessageType.CONTRACT_UPLOAD_CONFIRMATION_TO_FI)] + subject = _("New contract submission") + parameters = {"LOGIN_URL": f"{app_settings.frontend_url}/login"} + + case MessageType.CREDIT_DISBURSED: + recipients = [[application.primary_email]] + subject = _("Your credit application has been approved") + parameters = { + "LENDER_NAME": application.lender.name, + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + "LENDER_EMAIL": application.lender.email_group, + } + + case MessageType.OVERDUE_APPLICATION: + recipients = [[app_settings.ocp_email_group]] + subject = _("New overdue application") + parameters = { + "USER": application.lender.name, + "LENDER_NAME": application.lender.name, + "LOGIN_URL": f"{app_settings.frontend_url}/login", + } + + case MessageType.APPLICATION_COPIED: + recipients = [[application.primary_email]] + subject = _("Alternative credit option") + parameters = { + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + "CONTINUE_URL": f"{app_settings.frontend_url}/application/{application.uuid}/credit-options", + } + + case MessageType.EMAIL_CHANGE_CONFIRMATION: + recipients = [ + [application.primary_email], + [send_kwargs["new_email"]], + ] + subject = _("Confirm email address change") + parameters = { + "NEW_MAIL": send_kwargs["new_email"], + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + "CONFIRM_EMAIL_CHANGE_URL": ( + f"{app_settings.frontend_url}/application/{quote(application.uuid)}/change-primary-email" + f"?token={quote(send_kwargs['confirmation_email_token'])}" + ), + } + + case _: + raise NotImplementedError + + # If at least one email address is the borrower's, assume all are the borrower's. + to_borrower = [application.primary_email] in recipients + + # Only the last message ID is saved, if multiple email messages are sent. + for to_addresses in recipients: + message_id = send_email( + ses, to_addresses, get_template_data(template_name, subject, parameters), to_borrower=to_borrower + ) + + if save: + if message_kwargs is None: + message_kwargs = {} + Message.create( + session, application=application, type=message_type, external_message_id=message_id, **message_kwargs + ) def send_email(ses: SESClient, emails: list[str], data: dict[str, str], *, to_borrower: bool = True) -> str: @@ -65,9 +226,11 @@ def send_email(ses: SESClient, emails: list[str], data: dict[str, str], *, to_bo to_addresses = emails else: to_addresses = [app_settings.test_mail_receiver] + if not to_addresses: logger.error("No email address provided!") # ideally, it should be impossible for a lender to have no users return "" + logger.info("%s - Email to: %s sent to %s", app_settings.environment, emails, to_addresses) return ses.send_templated_email( Source=app_settings.email_sender_address, @@ -78,86 +241,10 @@ def send_email(ses: SESClient, emails: list[str], data: dict[str, str], *, to_bo )["MessageId"] -def get_lender_emails(lender: Lender, message_type: MessageType) -> list[str]: +def _get_lender_emails(lender: Lender, message_type: MessageType) -> list[str]: return [user.email for user in lender.users if user.notification_preferences.get(message_type)] -def send_approved_application(ses: SESClient, application: Application) -> str: - """ - Sends an email notification when an application has been approved. - - This function generates an email message with the application details and a - link to upload the contract. The email is sent to the primary email address associated - with the application. The function utilizes the SES (Simple Email Service) client to send the email. - """ - parameters = { - "LENDER_NAME": application.lender.name, - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - "TENDER_TITLE": application.award.title, - "BUYER_NAME": application.award.buyer_name, - "UPLOAD_CONTRACT_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/upload-contract", - } - - if application.lender.default_pre_approval_message: - parameters["ADDITIONAL_COMMENTS"] = application.lender.default_pre_approval_message - elif ( - "additional_comments" in application.lender_approved_data - and application.lender_approved_data["additional_comments"] - ): - parameters["ADDITIONAL_COMMENTS"] = application.lender_approved_data["additional_comments"] - else: - parameters["ADDITIONAL_COMMENTS"] = "Ninguno" - - return send_email( - ses, - [application.primary_email], - get_template_data("Application_approved", _("Your credit application has been prequalified"), parameters), - ) - - -def send_submission_completed(ses: SESClient, application: Application) -> str: - """ - Sends an email notification when an application is submitted. - - The email is sent to the primary email address associated - with the application. The function utilizes the SES (Simple Email Service) client to send the email. - """ - return send_email( - ses, - [application.primary_email], - get_template_data( - "Application_submitted", - _("Application Submission Complete"), - { - "LENDER_NAME": application.lender.name, - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - }, - ), - ) - - -def send_credit_disbursed(ses: SESClient, application: Application) -> str: - """ - Sends an email notification when an application has the credit dibursed. - - The email is sent to the primary email address associated - with the application. The function utilizes the SES (Simple Email Service) client to send the email. - """ - return send_email( - ses, - [application.primary_email], - get_template_data( - "Application_credit_disbursed", - _("Your credit application has been approved"), - { - "LENDER_NAME": application.lender.name, - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - "LENDER_EMAIL": application.lender.email_group, - }, - ), - ) - - def send_new_user(ses: SESClient, *, name: str, username: str, temporary_password: str) -> str: """ Sends an email to a new user with a link to set their password. @@ -174,7 +261,7 @@ def send_new_user(ses: SESClient, *, name: str, username: str, temporary_passwor ses, [username], get_template_data( - "New_Account_Created", + "new_user", _("Welcome"), { "USER": name, @@ -188,79 +275,6 @@ def send_new_user(ses: SESClient, *, name: str, username: str, temporary_passwor ) -def send_contract_upload_confirmation_to_fi(ses: SESClient, application: Application) -> str: - """ - Sends an email to the lender to notify them of a new contract submission. - - This function generates an email message for the lender associated with - the application, notifying them that a new contract has been submitted and needs their review. - The email contains a link to login and review the contract. - """ - return send_email( - ses, - get_lender_emails(application.lender, MessageType.CONTRACT_UPLOAD_CONFIRMATION_TO_FI), - get_template_data( - "New_contract_submission", - _("New contract submission"), - { - "LOGIN_URL": f"{app_settings.frontend_url}/login", - }, - ), - to_borrower=False, - ) - - -def send_contract_upload_confirmation(ses: SESClient, application: Application) -> str: - """ - Sends an email to the borrower confirming the successful upload of the contract. - - This function generates an email message for the borrower associated with the application, - confirming that their contract has been successfully uploaded. - """ - return send_email( - ses, - [application.primary_email], - get_template_data( - "Contract_upload_confirmation", - _("Thank you for uploading the signed contract"), - { - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - "TENDER_TITLE": application.award.title, - "BUYER_NAME": application.award.buyer_name, - }, - ), - ) - - -def send_email_change_confirmation( - ses: SESClient, application: Application, *, new_email: str, confirmation_email_token: str -) -> str: - """ - Sends an email to confirm the new primary email for the borrower. - - This function generates and sends an email message to the new and old email addresses, - providing a link for the user to confirm the email change. - - :param new_email: The new email address to be set as the primary email. - :param confirmation_email_token: The token generated for confirming the email change. - """ - data = get_template_data( - "Confirm_email_address_change", - _("Confirm email address change"), - { - "NEW_MAIL": new_email, - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - "CONFIRM_EMAIL_CHANGE_URL": ( - f"{app_settings.frontend_url}/application/{quote(application.uuid)}/change-primary-email" - f"?token={quote(confirmation_email_token)}" - ), - }, - ) - - send_email(ses, [application.primary_email], data) - return send_email(ses, [new_email], data) - - def send_reset_password(ses: SESClient, *, username: str, temporary_password: str) -> str: """ Sends an email to a user with instructions to reset their password. @@ -275,7 +289,7 @@ def send_reset_password(ses: SESClient, *, username: str, temporary_password: st ses, [username], get_template_data( - "Reset_password", + "reset_password", _("Reset password"), { "USER_ACCOUNT": username, @@ -289,7 +303,7 @@ def send_reset_password(ses: SESClient, *, username: str, temporary_password: st ) -def get_invitation_email_parameters(application: Application) -> dict[str, str]: +def _get_borrower_invitation_parameters(application: Application) -> dict[str, str]: base_application_url = f"{app_settings.frontend_url}/application/{quote(application.uuid)}" base_fathom_url = "?utm_source=credere-intro&utm_medium=email&utm_campaign=" return { @@ -301,126 +315,6 @@ def get_invitation_email_parameters(application: Application) -> dict[str, str]: } -def send_borrower_invitation(ses: SESClient, application: Application) -> str: - """ - Sends an invitation email to the provided email address. - - This function sends an email containing an invitation to the recipient to join a credit scheme. - It also provides options to find out more or to decline the invitation. - """ - return send_email( - ses, - [application.primary_email], - get_template_data( - "Access_to_credit_scheme_for_MSMEs", - _("Opportunity to access MSME credit for being awarded a public contract"), - get_invitation_email_parameters(application), - ), - ) - - -def send_borrower_pending_application_reminder(ses: SESClient, application: Application) -> str: - """ - Sends an introductory reminder email to the provided email address. - - This function sends a reminder email to the recipient about an invitation to join a credit scheme. - The email also provides options to find out more or to decline the invitation. - """ - return send_email( - ses, - [application.primary_email], - get_template_data( - "Access_to_credit_reminder", - _("Opportunity to access MSME credit for being awarded a public contract"), - get_invitation_email_parameters(application), - ), - ) - - -def send_borrower_pending_submit_reminder(ses: SESClient, application: Application) -> str: - """ - Sends a submission reminder email to the provided email address. - - This function sends a reminder email to the recipient about a pending credit scheme application. - The email also provides options to apply for the credit or to decline the application. - """ - return send_email( - ses, - [application.primary_email], - get_template_data( - "Complete_application_reminder", - _("Reminder - Opportunity to access MSME credit for being awarded a public contract"), - { - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - "TENDER_TITLE": application.award.title, - "BUYER_NAME": application.award.buyer_name, - "APPLY_FOR_CREDIT_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/intro", - "REMOVE_ME_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/decline", - }, - ), - ) - - -def send_new_application_fi(ses: SESClient, lender: Lender) -> str: - """ - Sends a notification email about a new application to a lender's email group. - - :param lender: The lender to email. - """ - return send_email( - ses, - get_lender_emails(lender, MessageType.NEW_APPLICATION_FI), - get_template_data( - "FI_New_application_submission_FI_user", - _("New application submission"), - { - "LOGIN_URL": f"{app_settings.frontend_url}/login", - }, - ), - to_borrower=False, - ) - - -def send_new_application_ocp(ses: SESClient, application: Application) -> str: - """ - Sends a notification email about a new application to the Open Contracting Partnership's (OCP) email group. - """ - return send_email( - ses, - [app_settings.ocp_email_group], - get_template_data( - "New_application_submission_OCP_user", - _("New application submission"), - { - "LENDER_NAME": application.lender.name, - "LOGIN_URL": f"{app_settings.frontend_url}/login", - }, - ), - to_borrower=False, - ) - - -def send_fi_message(ses: SESClient, application: Application, *, message: str) -> str: - """ - Sends an email request to the borrower for additional data. - - :param message: Message content from the lender to be included in the email. - """ - return send_email( - ses, - [application.primary_email], - get_template_data( - "Request_data_to_SME", - _("New message from a financial institution"), - { - "LENDER_NAME": application.lender.name, - "LENDER_MESSAGE": message, - "LOGIN_DOCUMENTS_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/documents", - }, - ), - ) - - def send_overdue_application_to_lender(ses: SESClient, *, lender: Lender, amount: int) -> str: """ Sends an email notification to the lender about overdue applications. @@ -430,9 +324,9 @@ def send_overdue_application_to_lender(ses: SESClient, *, lender: Lender, amount """ return send_email( ses, - get_lender_emails(lender, MessageType.OVERDUE_APPLICATION), + _get_lender_emails(lender, MessageType.OVERDUE_APPLICATION), get_template_data( - "Overdue_application_FI", + "overdue_application_to_lender", _("You have credit applications that need processing"), { "USER": lender.name, @@ -442,96 +336,3 @@ def send_overdue_application_to_lender(ses: SESClient, *, lender: Lender, amount ), to_borrower=False, ) - - -def send_overdue_application(ses: SESClient, application: Application) -> str: - """ - Sends an email notification to the Open Contracting Partnership (OCP) about overdue applications. - """ - return send_email( - ses, - [app_settings.ocp_email_group], - get_template_data( - "Overdue_application_OCP_admin", - _("New overdue application"), - { - "USER": application.lender.name, - "LENDER_NAME": application.lender.name, - "LOGIN_URL": f"{app_settings.frontend_url}/login", - }, - ), - to_borrower=False, - ) - - -def send_rejected_application(ses: SESClient, application: Application, *, options: bool) -> str: - """ - Sends an email notification to the applicant when an application has been rejected. - """ - if options: - return send_email( - ses, - application.primary_email, - get_template_data( - "Application_declined", - _("Your credit application has been declined"), - { - "LENDER_NAME": application.lender.name, - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - "FIND_ALTENATIVE_URL": ( - f"{app_settings.frontend_url}/application/{quote(application.uuid)}/find-alternative-credit" - ), - }, - ), - ) - - return send_email( - ses, - [application.primary_email], - get_template_data( - "Application_declined_without_alternative", - _("Your credit application has been declined"), - { - "LENDER_NAME": application.lender.name, - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - }, - ), - ) - - -def send_application_copied(ses: SESClient, application: Application) -> str: - """ - Sends an email notification to the borrower when an application - has been copied, allowing them to continue with the application process. - """ - return send_email( - ses, - [application.primary_email], - get_template_data( - "alternative_credit_msme", - _("Alternative credit option"), - { - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - "CONTINUE_URL": f"{app_settings.frontend_url}/application/{application.uuid}/credit-options", - }, - ), - ) - - -def send_borrower_document_updated(ses: SESClient, application: Application) -> str: - """ - Sends an email notification to the lender to notify them that new - documents have been uploaded and are ready for their review. - """ - return send_email( - ses, - get_lender_emails(application.lender, MessageType.BORROWER_DOCUMENT_UPDATED), - get_template_data( - "FI_Documents_Updated_FI_user", - _("Application updated"), - { - "LOGIN_URL": f"{app_settings.frontend_url}/login", - }, - ), - to_borrower=False, - ) diff --git a/app/routers/guest/applications.py b/app/routers/guest/applications.py index 6cb82969..ee09c6f8 100644 --- a/app/routers/guest/applications.py +++ b/app/routers/guest/applications.py @@ -532,8 +532,8 @@ async def update_apps_send_notifications( application.borrower_submitted_at = datetime.now(application.created_at.tzinfo) application.pending_documents = False - mail.send_new_application_ocp(client.ses, application) - mail.send_new_application_fi(client.ses, application) + mail.send(session, client.ses, models.MessageType.NEW_APPLICATION_OCP, application, save=False) + mail.send(session, client.ses, models.MessageType.NEW_APPLICATION_FI, application, save=False) mail.send(session, client.ses, models.MessageType.SUBMISSION_COMPLETED, application) session.commit() diff --git a/email_templates/__init__.py b/email_templates/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/email_templates/alternative_credit_msme.html b/email_templates/application_copied.en.html similarity index 100% rename from email_templates/alternative_credit_msme.html rename to email_templates/application_copied.en.html diff --git a/email_templates/alternative_credit_msme_es.html b/email_templates/application_copied.es.html similarity index 100% rename from email_templates/alternative_credit_msme_es.html rename to email_templates/application_copied.es.html diff --git a/email_templates/Application_approved.html b/email_templates/approved_application.en.html similarity index 100% rename from email_templates/Application_approved.html rename to email_templates/approved_application.en.html diff --git a/email_templates/Application_approved_es.html b/email_templates/approved_application.es.html similarity index 100% rename from email_templates/Application_approved_es.html rename to email_templates/approved_application.es.html diff --git a/email_templates/FI_Documents_Updated_FI_user.html b/email_templates/borrower_document_updated.en.html similarity index 100% rename from email_templates/FI_Documents_Updated_FI_user.html rename to email_templates/borrower_document_updated.en.html diff --git a/email_templates/FI_Documents_Updated_FI_user_es.html b/email_templates/borrower_document_updated.es.html similarity index 100% rename from email_templates/FI_Documents_Updated_FI_user_es.html rename to email_templates/borrower_document_updated.es.html diff --git a/email_templates/Access_to_credit_scheme_for_MSMEs.html b/email_templates/borrower_invitation.en.html similarity index 100% rename from email_templates/Access_to_credit_scheme_for_MSMEs.html rename to email_templates/borrower_invitation.en.html diff --git a/email_templates/Access_to_credit_scheme_for_MSMEs_es.html b/email_templates/borrower_invitation.es.html similarity index 100% rename from email_templates/Access_to_credit_scheme_for_MSMEs_es.html rename to email_templates/borrower_invitation.es.html diff --git a/email_templates/Access_to_credit_reminder.html b/email_templates/borrower_pending_application_reminder.en.html similarity index 100% rename from email_templates/Access_to_credit_reminder.html rename to email_templates/borrower_pending_application_reminder.en.html diff --git a/email_templates/Access_to_credit_reminder_es.html b/email_templates/borrower_pending_application_reminder.es.html similarity index 100% rename from email_templates/Access_to_credit_reminder_es.html rename to email_templates/borrower_pending_application_reminder.es.html diff --git a/email_templates/Complete_application_reminder.html b/email_templates/borrower_pending_submit_reminder.en.html similarity index 100% rename from email_templates/Complete_application_reminder.html rename to email_templates/borrower_pending_submit_reminder.en.html diff --git a/email_templates/Complete_application_reminder_es.html b/email_templates/borrower_pending_submit_reminder.es.html similarity index 100% rename from email_templates/Complete_application_reminder_es.html rename to email_templates/borrower_pending_submit_reminder.es.html diff --git a/email_templates/Contract_upload_confirmation.html b/email_templates/contract_upload_confirmation.en.html similarity index 100% rename from email_templates/Contract_upload_confirmation.html rename to email_templates/contract_upload_confirmation.en.html diff --git a/email_templates/Contract_upload_confirmation_es.html b/email_templates/contract_upload_confirmation.es.html similarity index 100% rename from email_templates/Contract_upload_confirmation_es.html rename to email_templates/contract_upload_confirmation.es.html diff --git a/email_templates/New_contract_submission.html b/email_templates/contract_upload_confirmation_to_fi.en.html similarity index 100% rename from email_templates/New_contract_submission.html rename to email_templates/contract_upload_confirmation_to_fi.en.html diff --git a/email_templates/New_contract_submission_es.html b/email_templates/contract_upload_confirmation_to_fi.es.html similarity index 100% rename from email_templates/New_contract_submission_es.html rename to email_templates/contract_upload_confirmation_to_fi.es.html diff --git a/email_templates/Application_credit_disbursed.html b/email_templates/credit_disbursed.en.html similarity index 100% rename from email_templates/Application_credit_disbursed.html rename to email_templates/credit_disbursed.en.html diff --git a/email_templates/Application_credit_disbursed_es.html b/email_templates/credit_disbursed.es.html similarity index 100% rename from email_templates/Application_credit_disbursed_es.html rename to email_templates/credit_disbursed.es.html diff --git a/email_templates/Confirm_email_address_change.html b/email_templates/email_change_confirmation.en.html similarity index 100% rename from email_templates/Confirm_email_address_change.html rename to email_templates/email_change_confirmation.en.html diff --git a/email_templates/Confirm_email_address_change_es.html b/email_templates/email_change_confirmation.es.html similarity index 100% rename from email_templates/Confirm_email_address_change_es.html rename to email_templates/email_change_confirmation.es.html diff --git a/email_templates/Request_data_to_SME.html b/email_templates/fi_message.en.html similarity index 100% rename from email_templates/Request_data_to_SME.html rename to email_templates/fi_message.en.html diff --git a/email_templates/Request_data_to_SME_es.html b/email_templates/fi_message.es.html similarity index 100% rename from email_templates/Request_data_to_SME_es.html rename to email_templates/fi_message.es.html diff --git a/email_templates/FI_New_application_submission_FI_user.html b/email_templates/new_application_fi.en.html similarity index 100% rename from email_templates/FI_New_application_submission_FI_user.html rename to email_templates/new_application_fi.en.html diff --git a/email_templates/FI_New_application_submission_FI_user_es.html b/email_templates/new_application_fi.es.html similarity index 100% rename from email_templates/FI_New_application_submission_FI_user_es.html rename to email_templates/new_application_fi.es.html diff --git a/email_templates/New_application_submission_OCP_user.html b/email_templates/new_application_ocp.en.html similarity index 100% rename from email_templates/New_application_submission_OCP_user.html rename to email_templates/new_application_ocp.en.html diff --git a/email_templates/New_application_submission_OCP_user_es.html b/email_templates/new_application_ocp.es.html similarity index 100% rename from email_templates/New_application_submission_OCP_user_es.html rename to email_templates/new_application_ocp.es.html diff --git a/email_templates/New_Account_Created.html b/email_templates/new_user.en.html similarity index 100% rename from email_templates/New_Account_Created.html rename to email_templates/new_user.en.html diff --git a/email_templates/New_Account_Created_es.html b/email_templates/new_user.es.html similarity index 100% rename from email_templates/New_Account_Created_es.html rename to email_templates/new_user.es.html diff --git a/email_templates/Overdue_application_OCP_admin.html b/email_templates/overdue_application.en.html similarity index 100% rename from email_templates/Overdue_application_OCP_admin.html rename to email_templates/overdue_application.en.html diff --git a/email_templates/Overdue_application_OCP_admin_es.html b/email_templates/overdue_application.es.html similarity index 100% rename from email_templates/Overdue_application_OCP_admin_es.html rename to email_templates/overdue_application.es.html diff --git a/email_templates/Overdue_application_FI.html b/email_templates/overdue_application_to_lender.en.html similarity index 100% rename from email_templates/Overdue_application_FI.html rename to email_templates/overdue_application_to_lender.en.html diff --git a/email_templates/Overdue_application_FI_es.html b/email_templates/overdue_application_to_lender.es.html similarity index 100% rename from email_templates/Overdue_application_FI_es.html rename to email_templates/overdue_application_to_lender.es.html diff --git a/email_templates/Application_declined.html b/email_templates/rejected_application_alternatives.en.html similarity index 100% rename from email_templates/Application_declined.html rename to email_templates/rejected_application_alternatives.en.html diff --git a/email_templates/Application_declined_es.html b/email_templates/rejected_application_alternatives.es.html similarity index 100% rename from email_templates/Application_declined_es.html rename to email_templates/rejected_application_alternatives.es.html diff --git a/email_templates/Application_declined_without_alternative.html b/email_templates/rejected_application_no_alternatives.en.html similarity index 100% rename from email_templates/Application_declined_without_alternative.html rename to email_templates/rejected_application_no_alternatives.en.html diff --git a/email_templates/Application_declined_without_alternative_es.html b/email_templates/rejected_application_no_alternatives.es.html similarity index 100% rename from email_templates/Application_declined_without_alternative_es.html rename to email_templates/rejected_application_no_alternatives.es.html diff --git a/email_templates/Reset_password.html b/email_templates/reset_password.en.html similarity index 100% rename from email_templates/Reset_password.html rename to email_templates/reset_password.en.html diff --git a/email_templates/Reset_password_es.html b/email_templates/reset_password.es.html similarity index 100% rename from email_templates/Reset_password_es.html rename to email_templates/reset_password.es.html diff --git a/email_templates/Application_submitted.html b/email_templates/submission_completed.en.html similarity index 100% rename from email_templates/Application_submitted.html rename to email_templates/submission_completed.en.html diff --git a/email_templates/Application_submitted_es.html b/email_templates/submission_completed.es.html similarity index 100% rename from email_templates/Application_submitted_es.html rename to email_templates/submission_completed.es.html From 6260f82e216ba89b30982a31fe12625bb7026118 Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:18:59 -0400 Subject: [PATCH 3/8] chore(refactor): Merge get_template_data() into send_email() (renamed _send_email). --- app/mail.py | 136 +++++++++++++++++++++++++--------------------------- 1 file changed, 66 insertions(+), 70 deletions(-) diff --git a/app/mail.py b/app/mail.py index bbaff24f..35c3dde9 100644 --- a/app/mail.py +++ b/app/mail.py @@ -1,6 +1,5 @@ import json import logging -import os from pathlib import Path from typing import Any from urllib.parse import quote @@ -14,30 +13,7 @@ logger = logging.getLogger(__name__) -BASE_TEMPLATES_PATH = os.path.join(Path(__file__).absolute().parent.parent, "email_templates") - - -def get_template_data(template_name: str, subject: str, parameters: dict[str, Any]) -> dict[str, str]: - """ - Read the HTML file and replace its parameters (like ``BUYER_NAME``) to use as the ``{{CONTENT}}`` tag in the email - template, then return all tags required by the email template. - """ - with open( - os.path.join(BASE_TEMPLATES_PATH, f"{template_name}.{app_settings.email_template_lang}.html"), - encoding="utf-8", - ) as f: - html = f.read() - - parameters.setdefault("IMAGES_BASE_URL", app_settings.images_base_url) - for key, value in parameters.items(): - html = html.replace("{{%s}}" % key, str(value)) - - return { - "CONTENT": html, - "SUBJECT": f"Credere - {subject}", - "FRONTEND_URL": app_settings.frontend_url, - "IMAGES_BASE_URL": app_settings.images_base_url, - } +BASE_TEMPLATES_PATH = Path(__file__).absolute().parent.parent / "email_templates" def send( @@ -209,8 +185,13 @@ def send( # Only the last message ID is saved, if multiple email messages are sent. for to_addresses in recipients: - message_id = send_email( - ses, to_addresses, get_template_data(template_name, subject, parameters), to_borrower=to_borrower + message_id = _send_email( + ses, + to_addresses=to_addresses, + to_borrower=to_borrower, + subject=subject, + template_name=template_name, + parameters=parameters, ) if save: @@ -221,23 +202,44 @@ def send( ) -def send_email(ses: SESClient, emails: list[str], data: dict[str, str], *, to_borrower: bool = True) -> str: - if app_settings.environment == "production" or not to_borrower: - to_addresses = emails - else: +def _send_email( + ses: SESClient, + *, + to_addresses: list[str], + to_borrower: bool = True, + subject: str, + template_name: str, + parameters: dict[str, str], +) -> str: + original_addresses = to_addresses.copy() + + if app_settings.environment != "production" and to_borrower: to_addresses = [app_settings.test_mail_receiver] if not to_addresses: logger.error("No email address provided!") # ideally, it should be impossible for a lender to have no users return "" - logger.info("%s - Email to: %s sent to %s", app_settings.environment, emails, to_addresses) + # Read the HTML template and replace its parameters (like ``BUYER_NAME``). + parameters.setdefault("IMAGES_BASE_URL", app_settings.images_base_url) + content = (BASE_TEMPLATES_PATH / f"{template_name}.{app_settings.email_template_lang}.html").read_text() + for key, value in parameters.items(): + content = content.replace("{{%s}}" % key, value) + + logger.info("%s - Email to: %s sent to %s", app_settings.environment, original_addresses, to_addresses) return ses.send_templated_email( Source=app_settings.email_sender_address, Destination={"ToAddresses": to_addresses}, ReplyToAddresses=[app_settings.ocp_email_group], Template=f"credere-main-{app_settings.email_template_lang}", - TemplateData=json.dumps(data), + TemplateData=json.dumps( + { + "SUBJECT": f"Credere - {subject}", + "CONTENT": content, + "FRONTEND_URL": app_settings.frontend_url, + "IMAGES_BASE_URL": app_settings.images_base_url, + } + ), )["MessageId"] @@ -257,21 +259,19 @@ def send_new_user(ses: SESClient, *, name: str, username: str, temporary_passwor :param username: The username (email address) of the new user. :param temporary_password: The temporary password for the new user. """ - return send_email( + return _send_email( ses, - [username], - get_template_data( - "new_user", - _("Welcome"), - { - "USER": name, - "LOGIN_URL": ( - f"{app_settings.frontend_url}/create-password" - f"?key={quote(temporary_password)}&email={quote(username)}" - ), - }, - ), + to_addresses=[username], to_borrower=False, + subject=_("Welcome"), + template_name="new_user", + parameters={ + "USER": name, + "LOGIN_URL": ( + f"{app_settings.frontend_url}/create-password" + f"?key={quote(temporary_password)}&email={quote(username)}" + ), + }, ) @@ -285,21 +285,19 @@ def send_reset_password(ses: SESClient, *, username: str, temporary_password: st :param username: The username associated with the account for which the password is to be reset. :param temporary_password: A temporary password generated for the account. """ - return send_email( + return _send_email( ses, - [username], - get_template_data( - "reset_password", - _("Reset password"), - { - "USER_ACCOUNT": username, - "RESET_PASSWORD_URL": ( - f"{app_settings.frontend_url}/create-password" - f"?key={quote(temporary_password)}&email={quote(username)}" - ), - }, - ), + to_addresses=[username], to_borrower=False, + subject=_("Reset password"), + template_name="reset_password", + parameters={ + "USER_ACCOUNT": username, + "RESET_PASSWORD_URL": ( + f"{app_settings.frontend_url}/create-password" + f"?key={quote(temporary_password)}&email={quote(username)}" + ), + }, ) @@ -322,17 +320,15 @@ def send_overdue_application_to_lender(ses: SESClient, *, lender: Lender, amount :param lender: The overdue lender. :param amount: Number of overdue applications. """ - return send_email( + return _send_email( ses, - _get_lender_emails(lender, MessageType.OVERDUE_APPLICATION), - get_template_data( - "overdue_application_to_lender", - _("You have credit applications that need processing"), - { - "USER": lender.name, - "NUMBER_APPLICATIONS": amount, - "LOGIN_URL": f"{app_settings.frontend_url}/login", - }, - ), + to_addresses=_get_lender_emails(lender, MessageType.OVERDUE_APPLICATION), to_borrower=False, + subject=_("You have credit applications that need processing"), + template_name="overdue_application_to_lender", + parameters={ + "USER": lender.name, + "NUMBER_APPLICATIONS": str(amount), + "LOGIN_URL": f"{app_settings.frontend_url}/login", + }, ) From 4eebcb3080ba660215be6711e01ac31f3c02658f Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:23:18 -0400 Subject: [PATCH 4/8] chore(refactor): Merge _get_borrower_invitation_parameters() into send() --- app/mail.py | 45 +++++++++++++++++---------------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/app/mail.py b/app/mail.py index 35c3dde9..22fca0f3 100644 --- a/app/mail.py +++ b/app/mail.py @@ -30,17 +30,22 @@ def send( # If so, use new template names for each condition. template_name = message_type.lower() + base_application_url = f"{app_settings.frontend_url}/application/{quote(application.uuid)}" + # recipients is a list of lists. Each sublist is a `ToAddresses` parameter for an email message. match message_type: - case MessageType.BORROWER_INVITATION: + case MessageType.BORROWER_INVITATION | MessageType.BORROWER_PENDING_APPLICATION_REMINDER: recipients = [[application.primary_email]] subject = _("Opportunity to access MSME credit for being awarded a public contract") - parameters = _get_borrower_invitation_parameters(application) - case MessageType.BORROWER_PENDING_APPLICATION_REMINDER: - recipients = [[application.primary_email]] - subject = _("Opportunity to access MSME credit for being awarded a public contract") - parameters = _get_borrower_invitation_parameters(application) + base_fathom_url = "?utm_source=credere-intro&utm_medium=email&utm_campaign=" + return { + "AWARD_SUPPLIER_NAME": application.borrower.legal_name, + "TENDER_TITLE": application.award.title, + "BUYER_NAME": application.award.buyer_name, + "FIND_OUT_MORE_URL": f"{base_application_url}/intro{base_fathom_url}intro", + "REMOVE_ME_URL": f"{base_application_url}/decline{base_fathom_url}decline", + } case MessageType.BORROWER_PENDING_SUBMIT_REMINDER: recipients = [[application.primary_email]] @@ -49,8 +54,8 @@ def send( "AWARD_SUPPLIER_NAME": application.borrower.legal_name, "TENDER_TITLE": application.award.title, "BUYER_NAME": application.award.buyer_name, - "APPLY_FOR_CREDIT_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/intro", - "REMOVE_ME_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/decline", + "APPLY_FOR_CREDIT_URL": f"{base_application_url}/intro", + "REMOVE_ME_URL": f"{base_application_url}/decline", } case MessageType.SUBMISSION_COMPLETED: @@ -77,7 +82,7 @@ def send( parameters = { "LENDER_NAME": application.lender.name, "LENDER_MESSAGE": send_kwargs["message"], - "LOGIN_DOCUMENTS_URL": f"{app_settings.frontend_url}/application/{quote(application.uuid)}/documents", + "LOGIN_DOCUMENTS_URL": f"{base_application_url}/documents", } case MessageType.BORROWER_DOCUMENT_UPDATED: @@ -95,9 +100,7 @@ def send( if send_kwargs["options"]: template_name = "rejected_application_alternatives" - parameters["FIND_ALTENATIVE_URL"] = ( - f"{app_settings.frontend_url}/application/{quote(application.uuid)}/find-alternative-credit" - ) + parameters["FIND_ALTENATIVE_URL"] = f"{base_application_url}/find-alternative-credit" else: template_name = "rejected_application_no_alternatives" @@ -117,9 +120,7 @@ def send( "TENDER_TITLE": application.award.title, "BUYER_NAME": application.award.buyer_name, "ADDITIONAL_COMMENTS": additional_comments, - "UPLOAD_CONTRACT_URL": ( - f"{app_settings.frontend_url}/application/{quote(application.uuid)}/upload-contract" - ), + "UPLOAD_CONTRACT_URL": f"{base_application_url}/upload-contract", } case MessageType.CONTRACT_UPLOAD_CONFIRMATION: @@ -172,7 +173,7 @@ def send( "NEW_MAIL": send_kwargs["new_email"], "AWARD_SUPPLIER_NAME": application.borrower.legal_name, "CONFIRM_EMAIL_CHANGE_URL": ( - f"{app_settings.frontend_url}/application/{quote(application.uuid)}/change-primary-email" + f"{base_application_url}/change-primary-email" f"?token={quote(send_kwargs['confirmation_email_token'])}" ), } @@ -301,18 +302,6 @@ def send_reset_password(ses: SESClient, *, username: str, temporary_password: st ) -def _get_borrower_invitation_parameters(application: Application) -> dict[str, str]: - base_application_url = f"{app_settings.frontend_url}/application/{quote(application.uuid)}" - base_fathom_url = "?utm_source=credere-intro&utm_medium=email&utm_campaign=" - return { - "AWARD_SUPPLIER_NAME": application.borrower.legal_name, - "TENDER_TITLE": application.award.title, - "BUYER_NAME": application.award.buyer_name, - "FIND_OUT_MORE_URL": f"{base_application_url}/intro{base_fathom_url}intro", - "REMOVE_ME_URL": f"{base_application_url}/decline{base_fathom_url}decline", - } - - def send_overdue_application_to_lender(ses: SESClient, *, lender: Lender, amount: int) -> str: """ Sends an email notification to the lender about overdue applications. From f40e7ab608a6d1715b4c8f31922fa2b9fdcf3ca4 Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:26:56 -0400 Subject: [PATCH 5/8] chore(refactor): Rename message_kwargs to save_kwargs, to reduce ambiguity with message argument for FI_MESSAGE --- app/mail.py | 12 ++++++------ app/routers/applications.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/mail.py b/app/mail.py index 22fca0f3..602fdddd 100644 --- a/app/mail.py +++ b/app/mail.py @@ -23,16 +23,16 @@ def send( application: Application, *, save: bool = True, - message_kwargs: dict[str, Any] | None = None, + save_kwargs: dict[str, Any] | None = None, **send_kwargs: Any, ) -> None: - # The template name can be overridden by the match statement, if it is conditional on `send_kwargs`. + # `template_name` can be overridden by the match statement, if it is conditional on `send_kwargs`. # If so, use new template names for each condition. template_name = message_type.lower() base_application_url = f"{app_settings.frontend_url}/application/{quote(application.uuid)}" - # recipients is a list of lists. Each sublist is a `ToAddresses` parameter for an email message. + # `recipients` is a list of lists. Each sublist is a `ToAddresses` parameter for an email message. match message_type: case MessageType.BORROWER_INVITATION | MessageType.BORROWER_PENDING_APPLICATION_REMINDER: recipients = [[application.primary_email]] @@ -196,10 +196,10 @@ def send( ) if save: - if message_kwargs is None: - message_kwargs = {} + if save_kwargs is None: + save_kwargs = {} Message.create( - session, application=application, type=message_type, external_message_id=message_id, **message_kwargs + session, application=application, type=message_type, external_message_id=message_id, **save_kwargs ) diff --git a/app/routers/applications.py b/app/routers/applications.py index dd1c91e8..a379f78b 100644 --- a/app/routers/applications.py +++ b/app/routers/applications.py @@ -536,7 +536,7 @@ async def email_borrower( models.MessageType.FI_MESSAGE, application, message=payload.message, - message_kwargs={"body": payload.message, "lender_id": application.lender.id}, + save_kwargs={"body": payload.message, "lender_id": application.lender.id}, ) session.commit() From 88f9e59fa5647f5163bd8d100711731e6e4707cc Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:34:34 -0400 Subject: [PATCH 6/8] fix(refactor): Fix accidental return --- app/mail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/mail.py b/app/mail.py index 602fdddd..e95d2587 100644 --- a/app/mail.py +++ b/app/mail.py @@ -39,7 +39,7 @@ def send( subject = _("Opportunity to access MSME credit for being awarded a public contract") base_fathom_url = "?utm_source=credere-intro&utm_medium=email&utm_campaign=" - return { + parameters = { "AWARD_SUPPLIER_NAME": application.borrower.legal_name, "TENDER_TITLE": application.award.title, "BUYER_NAME": application.award.buyer_name, From e17b492b16f69795264c580e4a9a65e923517fc9 Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:11:55 -0400 Subject: [PATCH 7/8] chore: Document coupling with credere-frontend --- app/mail.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/mail.py b/app/mail.py index e95d2587..6fbcecd7 100644 --- a/app/mail.py +++ b/app/mail.py @@ -33,6 +33,8 @@ def send( base_application_url = f"{app_settings.frontend_url}/application/{quote(application.uuid)}" # `recipients` is a list of lists. Each sublist is a `ToAddresses` parameter for an email message. + # + # All URLs using `app_settings.frontend_url` are React routes in credere-frontend. match message_type: case MessageType.BORROWER_INVITATION | MessageType.BORROWER_PENDING_APPLICATION_REMINDER: recipients = [[application.primary_email]] From 9aca7017478144249af00d7acff6e04e77e429b9 Mon Sep 17 00:00:00 2001 From: James McKinney <26463+jpmckinney@users.noreply.github.com> Date: Tue, 10 Sep 2024 12:11:38 -0400 Subject: [PATCH 8/8] docs: Add developer comment about match statement --- app/mail.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/mail.py b/app/mail.py index 6fbcecd7..ce8499da 100644 --- a/app/mail.py +++ b/app/mail.py @@ -32,15 +32,17 @@ def send( base_application_url = f"{app_settings.frontend_url}/application/{quote(application.uuid)}" + # This match statement must set `recipients`, `subject` and `parameters`. + # # `recipients` is a list of lists. Each sublist is a `ToAddresses` parameter for an email message. # # All URLs using `app_settings.frontend_url` are React routes in credere-frontend. match message_type: case MessageType.BORROWER_INVITATION | MessageType.BORROWER_PENDING_APPLICATION_REMINDER: + base_fathom_url = "?utm_source=credere-intro&utm_medium=email&utm_campaign=" + recipients = [[application.primary_email]] subject = _("Opportunity to access MSME credit for being awarded a public contract") - - base_fathom_url = "?utm_source=credere-intro&utm_medium=email&utm_campaign=" parameters = { "AWARD_SUPPLIER_NAME": application.borrower.legal_name, "TENDER_TITLE": application.award.title,