Skip to content

Commit

Permalink
Merge pull request #397 from open-contracting/385-lender-notifications
Browse files Browse the repository at this point in the history
feat: add notification_preferences per user
  • Loading branch information
yolile authored Aug 24, 2024
2 parents da711a2 + b79ab1d commit cb744bb
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 38 deletions.
8 changes: 2 additions & 6 deletions app/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down
67 changes: 36 additions & 31 deletions app/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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),
)

Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand Down Expand Up @@ -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:
Expand All @@ -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"),
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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",
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand Down
6 changes: 6 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down
2 changes: 1 addition & 1 deletion app/routers/guest/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions migrations/versions/1fa1da5eb109_user_notification_preferences.py
Original file line number Diff line number Diff line change
@@ -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")
5 changes: 5 additions & 0 deletions tests/commands/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -120,6 +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)
if not started_application.lender.users:
started_application.lender.users.append(
User(notification_preferences={models.MessageType.OVERDUE_APPLICATION: True}, email="[email protected]")
)
session.commit()

with assert_change(mock_send_templated_email, "call_count", call_count):
Expand Down

0 comments on commit cb744bb

Please sign in to comment.