From 3ff20d3d4e8b3a591ebf13304cf6ec9009244234 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Sat, 24 Aug 2024 14:55:17 -0400 Subject: [PATCH 1/3] feat: add notification_preferences per user --- app/__main__.py | 8 +-- app/mail.py | 67 ++++++++++--------- app/models.py | 6 ++ app/routers/guest/applications.py | 2 +- ...1da5eb109_user_notification_preferences.py | 34 ++++++++++ 5 files changed, 79 insertions(+), 38 deletions(-) create mode 100644 migrations/versions/1fa1da5eb109_user_notification_preferences.py diff --git a/app/__main__.py b/app/__main__.py index 6647eb7d..0d0a086b 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -314,9 +314,6 @@ def sla_overdue_applications() -> None: # Email lenders if the SLA days are dwindling. if days_passed > application.lender.sla_days * app_settings.progress_to_remind_started_applications: - if "email" not in overdue_lenders[application.lender.id]: - overdue_lenders[application.lender.id]["email"] = application.lender.email_group - overdue_lenders[application.lender.id]["name"] = application.lender.name overdue_lenders[application.lender.id]["count"] += 1 @@ -337,9 +334,8 @@ def sla_overdue_applications() -> None: for lender_id, lender_data in overdue_lenders.items(): message_id = mail.send_overdue_application_email_to_lender( aws.ses_client, - lender_name=lender_data["name"], - lender_email=lender_data["email"], - amount=lender_data["count"], + models.Lender.get(session, id=int(lender_id)), + lender_data["count"], ) models.Message.create( session, diff --git a/app/mail.py b/app/mail.py index 54ee9e97..179d273b 100644 --- a/app/mail.py +++ b/app/mail.py @@ -8,7 +8,7 @@ from mypy_boto3_ses.client import SESClient from app.i18n import _ -from app.models import Application +from app.models import Application, Lender, MessageType from app.settings import app_settings logger = logging.getLogger(__name__) @@ -50,22 +50,28 @@ def get_template_data(template_name: str, subject: str, parameters: dict[str, An } -def send_email(ses: SESClient, email: str, data: dict[str, str], *, to_borrower: bool = True) -> str: +def send_email(ses: SESClient, email: list[str], data: dict[str, str], *, to_borrower: bool = True) -> str: if app_settings.environment == "production" or not to_borrower: to_address = email else: - to_address = app_settings.test_mail_receiver - + to_address = [app_settings.test_mail_receiver] + if not to_address: + logger.info("No email will be sent, no address provided") + return "" logger.info("%s - Email to: %s sent to %s", app_settings.environment, email, to_address) return ses.send_templated_email( Source=app_settings.email_sender_address, - Destination={"ToAddresses": [to_address]}, + Destination={"ToAddresses": to_address}, ReplyToAddresses=[app_settings.ocp_email_group], Template=f"credere-main-{app_settings.email_template_lang}", TemplateData=json.dumps(data), )["MessageId"] +def get_lender_emails(lender: Lender, message_type: MessageType): + return [user.email for user in lender.users if user.notification_preferences.get(message_type, None)] + + def send_application_approved_email(ses: SESClient, application: Application) -> str: """ Sends an email notification when an application has been approved. @@ -95,7 +101,7 @@ def send_application_approved_email(ses: SESClient, application: Application) -> return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data("Application_approved", _("Your credit application has been prequalified"), parameters), ) @@ -109,7 +115,7 @@ def send_application_submission_completed(ses: SESClient, application: Applicati """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "Application_submitted", _("Application Submission Complete"), @@ -130,7 +136,7 @@ def send_application_credit_disbursed(ses: SESClient, application: Application) """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "Application_credit_disbursed", _("Your credit application has been approved"), @@ -157,7 +163,7 @@ def send_mail_to_new_user(ses: SESClient, name: str, username: str, temporary_pa """ return send_email( ses, - username, + [username], get_template_data( "New_Account_Created", _("Welcome"), @@ -184,7 +190,7 @@ def send_upload_contract_notification_to_lender(ses: SESClient, application: App """ return send_email( ses, - application.lender.email_group, + get_lender_emails(application.lender, MessageType.CONTRACT_UPLOAD_CONFIRMATION_TO_FI), get_template_data( "New_contract_submission", _("New contract submission"), @@ -206,7 +212,7 @@ def send_upload_contract_confirmation(ses: SESClient, application: Application) """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "Contract_upload_confirmation", _("Thank you for uploading the signed contract"), @@ -246,7 +252,7 @@ def send_new_email_confirmation( ) send_email(ses, application.primary_email, data) - return send_email(ses, new_email, data) + return send_email(ses, [new_email], data) def send_mail_to_reset_password(ses: SESClient, username: str, temporary_password: str) -> str: @@ -261,7 +267,7 @@ def send_mail_to_reset_password(ses: SESClient, username: str, temporary_passwor """ return send_email( ses, - username, + [username], get_template_data( "Reset_password", _("Reset password"), @@ -301,7 +307,7 @@ def send_invitation_email(ses: SESClient, application: Application) -> str: """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "Access_to_credit_scheme_for_MSMEs", _("Opportunity to access MSME credit for being awarded a public contract"), @@ -319,7 +325,7 @@ def send_mail_intro_reminder(ses: SESClient, application: Application) -> str: """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "Access_to_credit_reminder", _("Opportunity to access MSME credit for being awarded a public contract"), @@ -337,7 +343,7 @@ def send_mail_submit_reminder(ses: SESClient, application: Application) -> str: """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "Complete_application_reminder", _("Reminder - Opportunity to access MSME credit for being awarded a public contract"), @@ -354,15 +360,15 @@ def send_mail_submit_reminder(ses: SESClient, application: Application) -> str: ) -def send_notification_new_app_to_lender(ses: SESClient, application: Application) -> str: +def send_notification_new_app_to_lender(ses: SESClient, lender: Lender) -> str: """ Sends a notification email about a new application to a lender's email group. - :param lender_email_group: List of email addresses belonging to the lender. + :param lender: The lender to email. """ return send_email( ses, - application.lender.email_group, + get_lender_emails(lender, MessageType.NEW_APPLICATION_FI), get_template_data( "FI_New_application_submission_FI_user", _("New application submission"), @@ -381,7 +387,7 @@ def send_notification_new_app_to_ocp(ses: SESClient, application: Application) - """ return send_email( ses, - app_settings.ocp_email_group, + [app_settings.ocp_email_group], get_template_data( "New_application_submission_OCP_user", _("New application submission"), @@ -403,7 +409,7 @@ def send_mail_request_to_borrower(ses: SESClient, application: Application, emai """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "Request_data_to_SME", _("New message from a financial institution"), @@ -417,22 +423,21 @@ def send_mail_request_to_borrower(ses: SESClient, application: Application, emai ) -def send_overdue_application_email_to_lender(ses: SESClient, lender_name: str, lender_email: str, amount: int) -> str: +def send_overdue_application_email_to_lender(ses: SESClient, lender: Lender, amount: int) -> str: """ Sends an email notification to the lender about overdue applications. - :param lender_name: Name of the recipient at the lender. - :param lender_email: Email address of the recipient at the lender. + :param lender: The overdue lender. :param amount: Number of overdue applications. """ return send_email( ses, - lender_email, + get_lender_emails(lender, MessageType.OVERDUE_APPLICATION), get_template_data( "Overdue_application_FI", _("You have credit applications that need processing"), { - "USER": lender_name, + "USER": lender.name, "NUMBER_APPLICATIONS": amount, "LOGIN_IMAGE_LINK": f"{LOCALIZED_IMAGES_BASE_URL}/logincompleteimage.png", "LOGIN_URL": f"{app_settings.frontend_url}/login", @@ -448,7 +453,7 @@ def send_overdue_application_email_to_ocp(ses: SESClient, application: Applicati """ return send_email( ses, - app_settings.ocp_email_group, + [app_settings.ocp_email_group], get_template_data( "Overdue_application_OCP_admin", _("New overdue application"), @@ -469,7 +474,7 @@ def send_rejected_application_email(ses: SESClient, application: Application) -> """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "Application_declined", _("Your credit application has been declined"), @@ -492,7 +497,7 @@ def send_rejected_application_email_without_alternatives(ses: SESClient, applica """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "Application_declined_without_alternative", _("Your credit application has been declined"), @@ -511,7 +516,7 @@ def send_copied_application_notification_to_borrower(ses: SESClient, application """ return send_email( ses, - application.primary_email, + [application.primary_email], get_template_data( "alternative_credit_msme", _("Alternative credit option"), @@ -531,7 +536,7 @@ def send_upload_documents_notifications_to_lender(ses: SESClient, application: A """ return send_email( ses, - application.lender.email_group, + get_lender_emails(application.lender, MessageType.BORROWER_DOCUMENT_UPDATED), get_template_data( "FI_Documents_Updated_FI_user", _("Application updated"), diff --git a/app/models.py b/app/models.py index 9d663eac..0a29dc85 100644 --- a/app/models.py +++ b/app/models.py @@ -1004,6 +1004,12 @@ class UserBase(SQLModel): language: str = Field(default="es", description="ISO 639-1 language code") #: The email address with which the user logs in and is contacted. email: str = Field(unique=True) + #: The MessageType the user wants to receive notifications about. The supported MessageTypes are: + #: - MessageType.NEW_APPLICATION_FI + #: - MessageType.OVERDUE_APPLICATION + #: - MessageType.BORROWER_DOCUMENT_UPDATED + #: - MessageType.CONTRACT_UPLOAD_CONFIRMATION_TO_FI + notification_preferences: dict[MessageType, bool] = Field(default_factory=dict, sa_type=JSON) #: The name by which the user is addressed in emails and identified in application action histories. name: str = Field(default="") #: The Cognito ``Username``. diff --git a/app/routers/guest/applications.py b/app/routers/guest/applications.py index 2c2c32bf..d783f089 100644 --- a/app/routers/guest/applications.py +++ b/app/routers/guest/applications.py @@ -532,7 +532,7 @@ 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) + 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) diff --git a/migrations/versions/1fa1da5eb109_user_notification_preferences.py b/migrations/versions/1fa1da5eb109_user_notification_preferences.py new file mode 100644 index 00000000..7a6b27ec --- /dev/null +++ b/migrations/versions/1fa1da5eb109_user_notification_preferences.py @@ -0,0 +1,34 @@ +"""user notification preferences + +Revision ID: 1fa1da5eb109 +Revises: 755091b6b4b4 +Create Date: 2024-08-24 14:10:07.389323 + +""" + +from alembic import op +import sqlalchemy as sa + +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "1fa1da5eb109" +down_revision = "755091b6b4b4" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "credere_user", + sa.Column( + "notification_preferences", + postgresql.JSON(astext_type=sa.Text()), + nullable=False, + server_default=sa.text("'{}'::json"), + ), + ) + + +def downgrade() -> None: + op.drop_column("credere_user", "notification_preferences") From 01e9174de41bb0285901441da1dffc8651d28ab2 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Sat, 24 Aug 2024 15:19:10 -0400 Subject: [PATCH 2/3] tests: fix send overdue reminders test --- tests/commands/test_commands.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 6d3a79dd..c1e4b0c1 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -4,6 +4,7 @@ from typer.testing import CliRunner from app import __main__, models +from app.models import User from app.settings import app_settings from tests import assert_change, assert_success @@ -120,6 +121,12 @@ def test_send_overdue_reminders( reset_database, session, mock_send_templated_email, started_application, seconds, call_count, overdue ): started_application.lender_started_at = datetime.now(started_application.tz) - timedelta(seconds=seconds) + user = User( + notification_preferences={models.MessageType.OVERDUE_APPLICATION: True}, + lender=started_application.lender, + email="test@example.com", + ) + User.create_from_object(session, user) session.commit() with assert_change(mock_send_templated_email, "call_count", call_count): From b79ab1de4aef15dd2edff85246ca7f0ae6a21496 Mon Sep 17 00:00:00 2001 From: Yohanna Lisnichuk Date: Sat, 24 Aug 2024 15:30:35 -0400 Subject: [PATCH 3/3] fix test_send_overdue_reminders --- tests/commands/test_commands.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index c1e4b0c1..59282c67 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -121,12 +121,10 @@ def test_send_overdue_reminders( reset_database, session, mock_send_templated_email, started_application, seconds, call_count, overdue ): started_application.lender_started_at = datetime.now(started_application.tz) - timedelta(seconds=seconds) - user = User( - notification_preferences={models.MessageType.OVERDUE_APPLICATION: True}, - lender=started_application.lender, - email="test@example.com", - ) - User.create_from_object(session, user) + if not started_application.lender.users: + started_application.lender.users.append( + User(notification_preferences={models.MessageType.OVERDUE_APPLICATION: True}, email="test@test.com") + ) session.commit() with assert_change(mock_send_templated_email, "call_count", call_count):