Skip to content

Commit cc63dd5

Browse files
authored
32485 - Email notification for new accounts (#3642)
1 parent 88044ab commit cc63dd5

18 files changed

+220
-71
lines changed

auth-api/poetry.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

auth-api/src/auth_api/resources/v1/task.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,7 @@ def put_task(task_id):
8989
task = TaskService(TaskModel.find_by_task_id(task_id))
9090
if task:
9191
# Update task and its relationships
92-
origin = request.environ.get("HTTP_ORIGIN", "localhost")
93-
task_dict = task.update_task(task_info=request_json, origin_url=origin).as_dict()
92+
task_dict = task.update_task(task_info=request_json).as_dict()
9493
# ProductService uses TaskService already. So, we need to avoid circular import.
9594
if task_dict["relationship_type"] == TaskRelationshipType.PRODUCT.value:
9695
ProductService.update_org_product_keycloak_groups(task_dict["account_id"])

auth-api/src/auth_api/services/membership.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ def send_notification_to_member(self, origin_url, notification_type):
196196
"orgName": org_name,
197197
"role": self._model.membership_type.code,
198198
"label": self._model.membership_type.label,
199+
"loginSource": self._model.user.login_source,
199200
}
200201
elif notification_type == NotificationType.MEMBERSHIP_APPROVED.value:
201202
# TODO how to check properly if user is bceid user

auth-api/src/auth_api/services/org.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ def create_org(org_info: dict, user_id):
162162

163163
if is_staff_review_needed:
164164
Org._create_staff_review_task(org, UserModel.find_by_jwt_token())
165+
else:
166+
Org._send_account_created_notification(org, UserModel.find_by_jwt_token())
165167

166168
org.commit()
167169

@@ -200,6 +202,30 @@ def _create_staff_review_task(org: OrgModel, user: UserModel):
200202
TaskService.create_task(task_info=task_info, do_commit=False)
201203
Org.send_staff_review_account_reminder(relationship_id=org.id)
202204

205+
@staticmethod
206+
def _send_account_created_notification(org: OrgModel, user: UserModel):
207+
"""Send account created notification to the user."""
208+
current_app.logger.debug("<_send_account_created_notification")
209+
app_url = current_app.config.get("WEB_APP_URL")
210+
recipients = UserService.get_admin_emails_for_org(org.id)
211+
login_source = user.login_source
212+
if not recipients:
213+
current_app.logger.warning(f"No recipient found for org {org.id}")
214+
return
215+
216+
data = {
217+
"accountId": org.id,
218+
"orgName": org.name,
219+
"emailAddresses": recipients,
220+
"contextUrl": app_url,
221+
"loginSource": login_source,
222+
}
223+
try:
224+
publish_to_mailer(QueueMessageTypes.ACCOUNT_CREATED_NOTIFICATION.value, data=data)
225+
current_app.logger.debug("_send_account_created_notification>")
226+
except Exception as e: # noqa: B901
227+
current_app.logger.warning(f"_send_account_created_notification failed: {e}")
228+
203229
@staticmethod
204230
@user_context
205231
def create_membership(org, user_id, **kwargs):
@@ -954,7 +980,7 @@ def change_org_status(self, status_code, suspension_reason_code):
954980
return Org(org_model)
955981

956982
@staticmethod
957-
def approve_or_reject(org_id: int, is_approved: bool, origin_url: str = None, task_action: str = None):
983+
def approve_or_reject(org_id: int, is_approved: bool, task_action: str = None):
958984
"""Mark the affidavit as approved or rejected."""
959985
current_app.logger.debug("<find_affidavit_by_org_id ")
960986
# Get the org and check what's the current status
@@ -981,10 +1007,10 @@ def approve_or_reject(org_id: int, is_approved: bool, origin_url: str = None, ta
9811007
admin_emails = UserService.get_admin_emails_for_org(org_id)
9821008
if admin_emails != "":
9831009
if org.access_type in (AccessType.EXTRA_PROVINCIAL.value, AccessType.REGULAR_BCEID.value):
984-
Org.send_approved_rejected_notification(admin_emails, org.name, org.id, org.status_code, origin_url)
1010+
Org.send_approved_rejected_notification(admin_emails, org.name, org.id, org.status_code, user)
9851011
elif org.access_type in (AccessType.GOVM.value, AccessType.GOVN.value):
9861012
Org.send_approved_rejected_govm_govn_notification(
987-
admin_emails, org.name, org.id, org.status_code, origin_url
1013+
admin_emails, org.name, org.id, org.status_code, user
9881014
)
9891015
else:
9901016
# continue but log error
@@ -1025,7 +1051,7 @@ def send_staff_review_account_reminder(relationship_id, task_relationship_type=T
10251051
raise BusinessException(Error.FAILED_NOTIFICATION, None) from e
10261052

10271053
@staticmethod
1028-
def send_approved_rejected_notification(receipt_admin_emails, org_name, org_id, org_status: OrgStatus, origin_url):
1054+
def send_approved_rejected_notification(receipt_admin_emails, org_name, org_id, org_status: OrgStatus, user: UserModel = None):
10291055
"""Send Approved/Rejected notification to the user."""
10301056
current_app.logger.debug("<send_approved_rejected_notification")
10311057

@@ -1035,8 +1061,14 @@ def send_approved_rejected_notification(receipt_admin_emails, org_name, org_id,
10351061
notification_type = QueueMessageTypes.NON_BCSC_ORG_REJECTED_NOTIFICATION.value
10361062
else:
10371063
return # Don't send mail for any other status change
1038-
app_url = f"{origin_url}/"
1039-
data = {"accountId": org_id, "emailAddresses": receipt_admin_emails, "contextUrl": app_url, "orgName": org_name}
1064+
app_url = current_app.config.get("WEB_APP_URL")
1065+
data = {
1066+
"accountId": org_id,
1067+
"emailAddresses": receipt_admin_emails,
1068+
"contextUrl": app_url,
1069+
"orgName": org_name,
1070+
"loginSource": user.login_source
1071+
}
10401072
try:
10411073
publish_to_mailer(notification_type, data=data)
10421074
current_app.logger.debug("<send_approved_rejected_notification")
@@ -1046,7 +1078,7 @@ def send_approved_rejected_notification(receipt_admin_emails, org_name, org_id,
10461078

10471079
@staticmethod
10481080
def send_approved_rejected_govm_govn_notification(
1049-
receipt_admin_email, org_name, org_id, org_status: OrgStatus, origin_url
1081+
receipt_admin_email, org_name, org_id, org_status: OrgStatus, origin_url, user: UserModel
10501082
):
10511083
"""Send Approved govm notification to the user."""
10521084
current_app.logger.debug("<send_approved_rejected_govm_govn_notification")
@@ -1058,7 +1090,7 @@ def send_approved_rejected_govm_govn_notification(
10581090
else:
10591091
return # Don't send mail for any other status change
10601092
app_url = f"{origin_url}/"
1061-
data = {"accountId": org_id, "emailAddresses": receipt_admin_email, "contextUrl": app_url, "orgName": org_name}
1093+
data = {"accountId": org_id, "emailAddresses": receipt_admin_email, "contextUrl": app_url, "orgName": org_name, "loginSource": user.login_source}
10621094
try:
10631095
publish_to_mailer(notification_type, data=data)
10641096
current_app.logger.debug("send_approved_rejected_govm_govn_notification>")

auth-api/src/auth_api/services/task.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ def close_task(task_id, remarks: [] = None, do_commit: bool = True):
9090
if do_commit:
9191
db.session.commit()
9292

93-
def update_task(self, task_info: dict = None, origin_url: str = None):
93+
def update_task(self, task_info: dict = None):
9494
"""Update a task record."""
9595
current_app.logger.debug("<update_task ")
9696
task_model: TaskModel = self._model
@@ -104,12 +104,12 @@ def update_task(self, task_info: dict = None, origin_url: str = None):
104104
task_model.relationship_status = task_relationship_status
105105
task_model.flush()
106106

107-
self._update_relationship(origin_url=origin_url)
107+
self._update_relationship()
108108
db.session.commit()
109109
current_app.logger.debug(">update_task ")
110110
return Task(task_model)
111111

112-
def _update_relationship(self, origin_url: str = None):
112+
def _update_relationship(self):
113113
"""Retrieve the relationship record and update the status."""
114114
task_model: TaskModel = self._model
115115
current_app.logger.debug("<update_task_relationship ")
@@ -121,7 +121,7 @@ def _update_relationship(self, origin_url: str = None):
121121
org_id = task_model.relationship_id
122122
if not is_hold:
123123
self._update_org(
124-
is_approved=is_approved, org_id=org_id, origin_url=origin_url, task_action=task_model.action
124+
is_approved=is_approved, org_id=org_id, task_action=task_model.action
125125
)
126126
else:
127127
# Task with ACCOUNT_REVIEW action cannot be put on hold
@@ -211,7 +211,7 @@ def _notify_admin_about_hold(
211211
"applicationDate": f"{task_model.created.strftime('%m/%d/%Y')}",
212212
"accountId": account_id,
213213
"emailAddresses": admin_emails,
214-
"contextUrl": f"{current_app.config.get('WEB_APP_URL')}"
214+
"contextUrl": f"{current_app.config.get("WEB_APP_URL")}"
215215
f"/{current_app.config.get('BCEID_SIGNIN_ROUTE')}/"
216216
f"{create_account_signin_route}",
217217
}
@@ -223,14 +223,14 @@ def _notify_admin_about_hold(
223223
raise BusinessException(Error.FAILED_NOTIFICATION, None) from e
224224

225225
@staticmethod
226-
def _update_org(is_approved: bool, org_id: int, origin_url: str = None, task_action: str = None):
226+
def _update_org(is_approved: bool, org_id: int, task_action: str = None):
227227
"""Approve/Reject Affidavit and Org."""
228228
from auth_api.services import Org as OrgService # pylint:disable=cyclic-import, import-outside-toplevel
229229

230230
current_app.logger.debug("<update_task_org ")
231231

232232
OrgService.approve_or_reject(
233-
org_id=org_id, is_approved=is_approved, origin_url=origin_url, task_action=task_action
233+
org_id=org_id, is_approved=is_approved, task_action=task_action
234234
)
235235

236236
current_app.logger.debug(">update_task_org ")

auth-api/tests/unit/api/test_org.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import pytest
2727
import pytz
2828
from faker import Faker
29+
from sbc_common_components.utils.enums import QueueMessageTypes
2930

31+
import auth_api.utils.account_mailer
3032
from auth_api.exceptions import BusinessException
3133
from auth_api.exceptions.errors import Error
3234
from auth_api.models import Affidavit as AffidavitModel
@@ -81,6 +83,7 @@
8183
patch_pay_account_delete_error,
8284
patch_pay_account_fees,
8385
patch_pay_account_post,
86+
patch_pay_account_put,
8487
)
8588

8689
FAKE = Faker()
@@ -854,6 +857,35 @@ def test_add_org_invalid_returns_exception(client, jwt, session): # pylint:disa
854857
assert schema_utils.validate(rv.json, "exception")[0]
855858

856859

860+
@pytest.mark.parametrize(
861+
"route,org_data_factory",
862+
[
863+
("/api/v1/orgs", lambda: TestOrgInfo.org_govm), # GOVM: is_staff_review_needed=False
864+
("/api/v2/orgs", lambda: {**TestOrgInfo.org_govm, "contact": TestContactInfo.contact1}),
865+
],
866+
)
867+
@patch.object(UserService, "get_admin_emails_for_org", return_value="test@test.com")
868+
@patch.object(auth_api.services.org, "publish_to_mailer")
869+
def test_add_org_sends_account_created_notification(
870+
mock_mailer, _mock_admin_emails, client, jwt, session, keycloak_mock, monkeypatch, route, org_data_factory
871+
): # pylint:disable=unused-argument
872+
"""Assert that POST org (V1 and V2) sends account created notification."""
873+
patch_pay_account_post(monkeypatch)
874+
patch_pay_account_put(monkeypatch)
875+
headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.staff_admin_role)
876+
client.post("/api/v1/users", headers=headers, content_type="application/json")
877+
878+
org_data = org_data_factory()
879+
rv = client.post(route, data=json.dumps(org_data), headers=headers, content_type="application/json")
880+
assert rv.status_code == HTTPStatus.CREATED
881+
mock_mailer.assert_called_once()
882+
call_args = mock_mailer.call_args
883+
assert call_args[0][0] == QueueMessageTypes.ACCOUNT_CREATED_NOTIFICATION.value
884+
assert call_args[1]["data"]["emailAddresses"]
885+
assert call_args[1]["data"]["accountId"] == rv.json["id"]
886+
assert "orgName" in call_args[1]["data"]
887+
888+
857889
def test_get_org(client, jwt, session, keycloak_mock): # pylint:disable=unused-argument
858890
"""Assert that an org can be retrieved via GET."""
859891
headers = factory_auth_header(jwt=jwt, claims=TestJwtClaims.public_user_role)

auth-api/tests/unit/services/test_invitation.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,10 @@ def test_accept_invitation_for_govm(session, auth_mock, keycloak_mock, monkeypat
246246
with patch.object(InvitationService, "send_invitation", return_value=None):
247247
with patch.object(auth, "check_auth", return_value=True):
248248
with patch.object(InvitationService, "notify_admin", return_value=None):
249-
user_with_token = dict(TestUserInfo.user_staff_admin)
250-
user_with_token["keycloak_guid"] = TestJwtClaims.public_user_role["sub"]
251-
user = factory_user_model(user_with_token)
249+
staff_creator_with_token = dict(TestUserInfo.user_staff_admin)
250+
staff_creator_with_token["keycloak_guid"] = TestJwtClaims.staff_admin_role["sub"]
251+
staff_creator_with_token["idp_userid"] = TestJwtClaims.staff_admin_role["idp_userid"]
252+
user = factory_user_model(staff_creator_with_token)
252253

253254
patch_token_info(TestJwtClaims.staff_admin_role, monkeypatch)
254255

auth-api/tests/unit/services/test_invitation_auth.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,11 @@ def test_change_authentication_non_govm(session, auth_mock, keycloak_mock, monke
274274
@mock.patch("auth_api.services.affiliation_invitation.RestService.get_service_account_token", mock_token)
275275
def test_invitation_govm(session, auth_mock, keycloak_mock, monkeypatch):
276276
"""Assert that government ministry organization invites can be accepted by IDIR only."""
277-
# Users setup
278-
staff_user = factory_user_model(TestUserInfo.user_staff_admin)
277+
# Users setup - staff_user must align with token (sub and idp_userid) for find_by_jwt_token
278+
staff_creator_with_token = dict(TestUserInfo.user_staff_admin)
279+
staff_creator_with_token["keycloak_guid"] = TestJwtClaims.staff_admin_role["sub"]
280+
staff_creator_with_token["idp_userid"] = TestJwtClaims.staff_admin_role["idp_userid"]
281+
staff_user = factory_user_model(staff_creator_with_token)
279282
staff_invitee_user = factory_user_model(TestUserInfo.user1)
280283
invitee_bcsc_user = factory_user_model(TestUserInfo.user2)
281284
invitee_bceid_user = factory_user_model(TestUserInfo.user3)

auth-api/tests/unit/services/test_org.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,10 @@ def test_create_basic_org_assert_pay_request_is_govm(session, keycloak_mock, sta
284284
"""Assert that while org creation , pay-api gets called with proper data for basic accounts."""
285285
user = factory_user_model()
286286
token_info = TestJwtClaims.get_test_user(
287-
sub=user.keycloak_guid, source=LoginSource.STAFF.value, roles=["create_accounts"]
287+
sub=user.keycloak_guid,
288+
source=LoginSource.STAFF.value,
289+
roles=["create_accounts"],
290+
idp_userid=user.idp_userid,
288291
)
289292
with patch.object(RestService, "post") as mock_post:
290293
patch_token_info(token_info, monkeypatch)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Your BC Registries and Online Services account has been approved.
2+
3+
Account Number: {{ account_number }}
4+
Account Name: {{ account_name_with_branch }}
5+
Login Method: {{ login_source }}
6+
7+
[Log in to your account]({{ context_url }})
8+
9+
---
10+
11+
**BC Registries and Online Services**
12+
Toll Free: 1-877-526-1526
13+
Victoria Office: 250-387-7848
14+
Email: [BCRegistries@gov.bc.ca](BCRegistries@gov.bc.ca)

0 commit comments

Comments
 (0)