Skip to content

Commit

Permalink
17528 - Qualified supplier confirmation email (#2538)
Browse files Browse the repository at this point in the history
* 17528 - Qualified Supplier Email - Confirmation

* PDF attachment fix
  • Loading branch information
ochiu authored Sep 14, 2023
1 parent 4408a1e commit 5657645
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 10 deletions.
12 changes: 12 additions & 0 deletions auth-api/src/auth_api/services/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ def create_product_subscription(org_id, subscription_data: Dict[str, Any], # py
user_id=user.id,
external_source_id=external_source_id
))
Product._send_product_subscription_confirmation(ProductNotificationInfo(
product_model=product_model,
product_sub_model=product_subscription,
is_confirmation=True
), org.id)

else:
raise BusinessException(Error.DATA_NOT_FOUND, None)
Expand All @@ -146,6 +151,13 @@ def create_product_subscription(org_id, subscription_data: Dict[str, Any], # py

return Product.get_all_product_subscription(org_id=org_id, skip_auth=True)

@staticmethod
def _send_product_subscription_confirmation(product_notification_info: ProductNotificationInfo, org_id: int):
admin_emails = UserService.get_admin_emails_for_org(org_id)
product_notification_info.recipient_emails = admin_emails

Product.send_product_subscription_notification(product_notification_info)

@staticmethod
def _update_parent_subscription(org_id, sub_product_model, subscription_status):
parent_code = sub_product_model.parent_code
Expand Down
2 changes: 1 addition & 1 deletion auth-api/src/auth_api/utils/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,6 @@ class NotificationTypes(Enum):

DEFAULT_APPROVED_PRODUCT = 'prodPackageApprovedNotification'
DEFAULT_REJECTED_PRODUCT = 'prodPackageRejectedNotification'
DETAILED_CONFIRMATION_PRODUCT = 'productConfirmationNotificationDetailed'
DETAILED_CONFIRMATION_PRODUCT = 'productConfirmationNotification'
DETAILED_APPROVED_PRODUCT = 'productApprovedNotificationDetailed'
DETAILED_REJECTED_PRODUCT = 'productRejectedNotificationDetailed'
40 changes: 37 additions & 3 deletions auth-api/src/auth_api/utils/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ class ProductNotificationInfo:

product_model: ProductCodeModel
product_sub_model: ProductSubscriptionModel
recipient_emails: str
recipient_emails: Optional[str] = None
remarks: Optional[str] = None
is_reapproved: Optional[bool] = False
is_confirmation: Optional[bool] = False


# e.g [BC Registries and Online Services] Your {{MHR_QUALIFIED_SUPPLIER}} Access Has Been Approved
Expand All @@ -70,10 +71,17 @@ class ProductCategoryDescriptor(Enum):
MHR = 'the Manufactured Home Registry'


class NotificationAttachmentType(Enum):
"""Notification attachment type."""

MHR_QS = 'QUALIFIED_SUPPLIER'


def get_product_notification_type(product_notification_info: ProductNotificationInfo):
"""Get the appropriate product notification type."""
product_model = product_notification_info.product_model
is_reapproved = product_notification_info.is_reapproved
is_confirmation = product_notification_info.is_confirmation
subscription_status_code = product_notification_info.product_sub_model.status_code

# Use detailed version of product subscription notification templates
Expand All @@ -84,6 +92,9 @@ def get_product_notification_type(product_notification_info: ProductNotification
if subscription_status_code == ProductSubscriptionStatus.REJECTED.value:
return NotificationTypes.DETAILED_REJECTED_PRODUCT.value

if is_confirmation:
return NotificationTypes.DETAILED_CONFIRMATION_PRODUCT.value

# Use default product subscription notification templates
if subscription_status_code == ProductSubscriptionStatus.ACTIVE.value:
return NotificationTypes.DEFAULT_APPROVED_PRODUCT.value
Expand All @@ -99,12 +110,16 @@ def get_product_notification_data(product_notification_info: ProductNotification
product_model = product_notification_info.product_model
recipient_emails = product_notification_info.recipient_emails
is_reapproved = product_notification_info.is_reapproved
is_confirmation = product_notification_info.is_confirmation
subscription_status_code = product_notification_info.product_sub_model.status_code
remarks = product_notification_info.remarks

if product_model.code not in DETAILED_MHR_NOTIFICATIONS:
return get_default_product_notification_data(product_model, recipient_emails)

if is_confirmation:
return get_mhr_qs_confirmation_data(product_model, recipient_emails)

if is_reapproved or subscription_status_code == ProductSubscriptionStatus.ACTIVE.value:
return get_mhr_qs_approval_data(product_model, recipient_emails, is_reapproved)

Expand Down Expand Up @@ -138,7 +153,6 @@ def get_mhr_qs_approval_data(product_model: ProductCodeModel, recipient_emails:

def get_mhr_qs_rejected_data(product_model: ProductCodeModel, recipient_emails: str, reject_reason: str = None):
"""Get the mhr qualified supplier product rejected notification data."""
contact_type = 'BCOL' if product_model.code == ProductCode.MHR_QSLN.value else 'BCREG'
data = {
'subjectDescriptor': ProductSubjectDescriptor.MHR_QUALIFIED_SUPPLIER.value,
'productAccessDescriptor': ProductAccessDescriptor.MHR_QUALIFIED_SUPPLIER.value,
Expand All @@ -147,6 +161,26 @@ def get_mhr_qs_rejected_data(product_model: ProductCodeModel, recipient_emails:
'productName': product_model.description,
'emailAddresses': recipient_emails,
'remarks': reject_reason,
'contactType': contact_type
'contactType': get_notification_contact_type(product_model.code)
}
return data


def get_mhr_qs_confirmation_data(product_model: ProductCodeModel, recipient_emails: str):
"""Get the mhr qualified supplier product confirmation notification data."""
data = {
'subjectDescriptor': ProductSubjectDescriptor.MHR_QUALIFIED_SUPPLIER.value,
'productAccessDescriptor': ProductAccessDescriptor.MHR_QUALIFIED_SUPPLIER.value,
'categoryDescriptor': ProductCategoryDescriptor.MHR.value,
'productName': product_model.description,
'emailAddresses': recipient_emails,
'contactType': get_notification_contact_type(product_model.code),
'hasAgreementAttachment': True,
'attachmentType': NotificationAttachmentType.MHR_QS.value,
}
return data


def get_notification_contact_type(product_code: str) -> str:
"""Get the notification contact type for a product."""
return 'BCOL' if product_code == ProductCode.MHR_QSLN.value else 'BCREG'
95 changes: 92 additions & 3 deletions auth-api/tests/unit/services/test_product_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
from auth_api.services.user import User as UserService
from auth_api.utils.enums import (
LoginSource, NotificationTypes, TaskAction, TaskRelationshipStatus, TaskRelationshipType, TaskStatus)
from auth_api.utils.notifications import ProductAccessDescriptor, ProductCategoryDescriptor, ProductSubjectDescriptor
from auth_api.utils.notifications import (
NotificationAttachmentType, ProductAccessDescriptor, ProductCategoryDescriptor, ProductSubjectDescriptor)
from tests.utilities.factory_scenarios import TestJwtClaims, TestOrgInfo, TestOrgProductsInfo, TestUserInfo
from tests.utilities.factory_utils import factory_user_model_with_contact, patch_token_info

Expand Down Expand Up @@ -347,7 +348,7 @@ def test_detailed_rejected_notification(mock_mailer, session, auth_mock, keycloa
])
@patch.object(auth_api.services.products, 'publish_to_mailer')
def test_hold_notification(mock_mailer, session, auth_mock, keycloak_mock, monkeypatch, org_product_info):
"""Assert product approved notification with details is created."""
"""Assert product notification is not created for on hold state."""
user_with_token = TestUserInfo.user_bceid_tester
user_with_token['keycloak_guid'] = TestJwtClaims.public_bceid_user['sub']
user_with_token['idp_userid'] = TestJwtClaims.public_bceid_user['idp_userid']
Expand Down Expand Up @@ -400,7 +401,7 @@ def test_hold_notification(mock_mailer, session, auth_mock, keycloak_mock, monke
assert task.relationship_id == org_prod_sub.id
assert task.action == TaskAction.QUALIFIED_SUPPLIER_REVIEW.value

# Approve task and check for publish to mailer
# Hold task and check publish to mailer is not called
task_info = {
'relationshipStatus': TaskRelationshipStatus.PENDING_STAFF_REVIEW.value,
'status': TaskStatus.HOLD.value
Expand All @@ -409,3 +410,91 @@ def test_hold_notification(mock_mailer, session, auth_mock, keycloak_mock, monke
with patch.object(UserService, 'get_admin_emails_for_org', return_value='[email protected]'):
TaskService.update_task(TaskService(task), task_info=task_info)
mock_mailer.assert_not_called


@pytest.mark.parametrize('org_product_info, contact_type', [
(TestOrgProductsInfo.mhr_qs_lawyer_and_notaries, 'BCOL'),
(TestOrgProductsInfo.mhr_qs_home_manufacturers, 'BCREG'),
(TestOrgProductsInfo.mhr_qs_home_dealers, 'BCREG')
])
@patch.object(auth_api.services.products, 'publish_to_mailer')
def test_confirmation_notification(mock_mailer, session, auth_mock, keycloak_mock,
monkeypatch, org_product_info, contact_type):
"""Assert product confirmation notification is properly created."""
user_with_token = TestUserInfo.user_bceid_tester
user_with_token['keycloak_guid'] = TestJwtClaims.public_bceid_user['sub']
user_with_token['idp_userid'] = TestJwtClaims.public_bceid_user['idp_userid']
user = factory_user_model_with_contact(user_with_token)

patch_token_info(TestJwtClaims.public_bceid_user, monkeypatch)

org = OrgService.create_org(TestOrgInfo.org_premium, user_id=user.id)
assert org
dictionary = org.as_dict()
assert dictionary['name'] == TestOrgInfo.org_premium['name']

product_code = org_product_info['subscriptions'][0]['productCode']
product_code_model = ProductCodeModel.find_by_code(product_code)

if product_code_model.parent_code:
# Create parent product subscription
ProductService.create_product_subscription(org_id=dictionary['id'],
subscription_data={'subscriptions': [
{'productCode': product_code_model.parent_code}]},
skip_auth=True)

with patch.object(UserService, 'get_admin_emails_for_org', return_value='[email protected]'):
# Subscribe to product
ProductService.create_product_subscription(org_id=dictionary['id'],
subscription_data=org_product_info,
skip_auth=True)

expected_data = {
'subjectDescriptor': ProductSubjectDescriptor.MHR_QUALIFIED_SUPPLIER.value,
'productAccessDescriptor': ProductAccessDescriptor.MHR_QUALIFIED_SUPPLIER.value,
'categoryDescriptor': ProductCategoryDescriptor.MHR.value,
'productName': product_code_model.description,
'emailAddresses': '[email protected]',
'contactType': contact_type,
'hasAgreementAttachment': True,
'attachmentType': NotificationAttachmentType.MHR_QS.value
}

mock_mailer.assert_called_with(NotificationTypes.DETAILED_CONFIRMATION_PRODUCT.value,
data=expected_data)


@pytest.mark.parametrize('org_product_info', [
TestOrgProductsInfo.org_products_vs
])
@patch.object(auth_api.services.products, 'publish_to_mailer')
def test_no_confirmation_notification(mock_mailer, session, auth_mock, keycloak_mock, monkeypatch, org_product_info):
"""Assert product confirmation notification not created."""
user_with_token = TestUserInfo.user_bceid_tester
user_with_token['keycloak_guid'] = TestJwtClaims.public_bceid_user['sub']
user_with_token['idp_userid'] = TestJwtClaims.public_bceid_user['idp_userid']
user = factory_user_model_with_contact(user_with_token)

patch_token_info(TestJwtClaims.public_bceid_user, monkeypatch)

org = OrgService.create_org(TestOrgInfo.org_premium, user_id=user.id)
assert org
dictionary = org.as_dict()
assert dictionary['name'] == TestOrgInfo.org_premium['name']

product_code = org_product_info['subscriptions'][0]['productCode']
product_code_model = ProductCodeModel.find_by_code(product_code)

if product_code_model.parent_code:
# Create parent product subscription
ProductService.create_product_subscription(org_id=dictionary['id'],
subscription_data={'subscriptions': [
{'productCode': product_code_model.parent_code}]},
skip_auth=True)

with patch.object(UserService, 'get_admin_emails_for_org', return_value='[email protected]'):
# Subscribe to product
ProductService.create_product_subscription(org_id=dictionary['id'],
subscription_data=org_product_info,
skip_auth=True)
mock_mailer.assert_not_called()
2 changes: 2 additions & 0 deletions queue_services/account-mailer/src/account_mailer/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ class _Config(): # pylint: disable=too-few-public-methods
AUTH_WEB_TOKEN_CONFIRM_PATH = os.getenv('AUTH_WEB_TOKEN_CONFIRM_PATH')
# PAD TOS PDF file name.
PAD_TOS_FILE = os.getenv('PAD_TOS_FILE', 'BCROS-Business-Pre-Authorized-Debit-Agreement.pdf')
# MHR QUALIFIED SUPPLIER PDF File name
MHR_QS_AGREEMENT_FILE = os.getenv('MHR_QS_AGREEMENT_FILE', 'MHR_QualifiedSuppliersAgreement.pdf')

# If any value is present in this flag, starts up a keycloak docker
USE_TEST_KEYCLOAK_DOCKER = os.getenv('USE_TEST_KEYCLOAK_DOCKER', None)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright © 2023 Province of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A processor for the product confirmation email."""

import base64

from flask import current_app

from account_mailer.enums import AttachmentTypes
from account_mailer.services import minio_service


def process_attachment(email_dict: dict, attachment_type: str) -> dict:
"""Process any attachments for a product confirmation notification."""
if attachment_type is None:
return email_dict

attachment_name = _get_attachment_name(attachment_type)

if attachment_name is None:
return email_dict

pdf_attachment = _get_pdf(attachment_name)
email_dict['content']['attachments'] = [
{
'fileName': attachment_name,
'fileBytes': pdf_attachment.decode('utf-8'),
'fileUrl': '',
'attachOrder': '1'
}
]

return email_dict


def _get_attachment_name(attachment_type: str) -> str:
if attachment_type == AttachmentTypes.QUALIFIED_SUPPLIER.value:
return current_app.config['MHR_QS_AGREEMENT_FILE']

return None


def _get_pdf(file_name: str):
read_pdf = None
mino_object = minio_service.MinioService.get_minio_file(current_app.config['MINIO_BUCKET'],
file_name)
if mino_object:
read_pdf = base64.b64encode(mino_object.data)

return read_pdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Your application for {{ product_access_descriptor }} access to {{ category_descriptor }} has been received.

BC Registries staff have received your application to have {{ product_name }} access to {{ category_descriptor }}, and your request is under review.

{% if has_agreement_attachment %}
Please find a copy of the {{ product_access_descriptor }} Agreement attached for your records.
{% endif %}

You will be notified by email once your application has been reviewed.

{% if contact_type == 'BCOL' %}
Please contact BC OnLine at 1-800-663-6102 or email [email protected] to discuss further actions.
{% else %}
Please contact BC Registries at 1-877-526-1526 or email [email protected] to discuss further actions.
{% endif %}
11 changes: 11 additions & 0 deletions queue_services/account-mailer/src/account_mailer/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class MessageType(Enum):
PROD_PACKAGE_REJECTED_NOTIFICATION = 'bc.registry.auth.prodPackageRejectedNotification'
PRODUCT_APPROVED_NOTIFICATION_DETAILED = 'bc.registry.auth.productApprovedNotificationDetailed'
PRODUCT_REJECTED_NOTIFICATION_DETAILED = 'bc.registry.auth.productRejectedNotificationDetailed'
PRODUCT_CONFIRMATION_NOTIFICATION = 'bc.registry.auth.productConfirmationNotification'
RESUBMIT_BCEID_ORG_NOTIFICATION = 'bc.registry.auth.resubmitBceidOrg'
RESUBMIT_BCEID_ADMIN_NOTIFICATION = 'bc.registry.auth.resubmitBceidAdmin'
AFFILIATION_INVITATION_REQUEST = 'bc.registry.auth.affiliationInvitationRequest'
Expand Down Expand Up @@ -106,6 +107,8 @@ class SubjectType(Enum):
'Access Has Been Approved'
PRODUCT_REJECTED_NOTIFICATION_DETAILED = '[BC Registries and Online Services] Your {subject_descriptor} ' \
'Access Has Been Rejected'
PRODUCT_CONFIRMATION_NOTIFICATION = '[BC Registries and Online Services] {subject_descriptor} ' \
'Application Confirmation'
RESUBMIT_BCEID_ORG_NOTIFICATION = '[BC Registries and Online Services] YOUR ACTION REQUIRED: ' \
'Update your information.'
RESUBMIT_BCEID_ADMIN_NOTIFICATION = '[BC Registries and Online Services] YOUR ACTION REQUIRED: ' \
Expand Down Expand Up @@ -137,6 +140,7 @@ class TitleType(Enum):
PROD_PACKAGE_REJECTED_NOTIFICATION = 'Your Product Request Has Been Rejected'
PRODUCT_APPROVED_NOTIFICATION_DETAILED = 'Your Product Request Has Been Approved'
PRODUCT_REJECTED_NOTIFICATION_DETAILED = 'Your Product Request Has Been Rejected'
PRODUCT_CONFIRMATION_NOTIFICATION = 'Your Product Request Application Has Been Received'
RESUBMIT_BCEID_ORG_NOTIFICATION = 'Your Account Creation Request is On hold '
RESUBMIT_BCEID_ADMIN_NOTIFICATION = 'Your Team Member Request is On hold '
AFFILIATION_INVITATION = 'Invitation to manage a business with your account.'
Expand Down Expand Up @@ -180,6 +184,7 @@ class TemplateType(Enum):
PROD_PACKAGE_REJECTED_NOTIFICATION_TEMPLATE_NAME = 'prod_package_rejected_notification'
PRODUCT_APPROVED_NOTIFICATION_DETAILED_TEMPLATE_NAME = 'product_approved_notification_detailed'
PRODUCT_REJECTED_NOTIFICATION_DETAILED_TEMPLATE_NAME = 'product_rejected_notification_detailed'
PRODUCT_CONFIRMATION_NOTIFICATION_TEMPLATE_NAME = 'product_confirmation_notification'
RESUBMIT_BCEID_ORG_NOTIFICATION_TEMPLATE_NAME = 'resubmit_bceid_org'
RESUBMIT_BCEID_ADMIN_NOTIFICATION_TEMPLATE_NAME = 'resubmit_bceid_admin'
AFFILIATION_INVITATION_REQUEST_TEMPLATE_NAME = 'affiliation_invitation_request'
Expand All @@ -190,3 +195,9 @@ class Constants(Enum):
"""Constants."""

RESET_PASSCODE_HEADER = 'BC Registries have generated a new passcode for your business.'


class AttachmentTypes(Enum):
"""Notification Attachment Types."""

QUALIFIED_SUPPLIER = 'QUALIFIED_SUPPLIER'
Loading

0 comments on commit 5657645

Please sign in to comment.