"""Logic layer for collaborators email notifications.""" import base64 import json import urllib.parse from typing import Any, Type from marshmallow import Schema from owsresponse import response, status as ows_status from segment import analytics from notifications import config from notifications.connectors import sqs from notifications.constants import email from notifications.models import label, subscriptions from notifications.types import FollowConfigs from notifications.validation.relationship import CollaboratorFollow def _generate_tracking_url(identity_id: str, recipient: str) -> str: """Generate a segment tracking URL. Args: identity_id (str): the orchard user ID. recipient (str): the email address of the recipient. Returns: str: the tracking URL. """ params = { 'writeKey': config.SEGMENT_WRITE_KEY, 'userId': identity_id, 'event': 'Email Opened', 'properties': {'subject': 'collaboratorsStatementPeriodClosed', 'email': recipient}, } data = base64.b64encode(json.dumps(params).encode()).decode('utf-8') return f'{email.SEGMENT_TRACKING_URL}?data={data}' def _generate_accounting_url(identity_id: str, statement_id: str, brand: str | None) -> str: """Generate an accounting URL. Args: identity_id (str): the orchard user ID. statement_id (str): the statement period ID. brand (str): the brand of the user. Returns: str: the tracking URL. """ if brand == 'awal': prod_url = 'https://accounting.awal.com' qa_url = 'https://moneyhub.qaawal.com' else: prod_url = 'https://accounting.theorchard.com' qa_url = 'https://moneyhub.qaorch.com' if config.ENVIRONMENT == config.PROD_ENVIRONMENT: accounting_url = prod_url else: accounting_url = qa_url qs = urllib.parse.urlencode( {'forwardedViaEmailIdentityId': identity_id, 'forwardedViaEmailStatementId': statement_id} ) return f'{accounting_url}?{qs}' def _get_following_identities( follow_configs: list[tuple[Type[Schema], int]], ) -> list[dict[str, Any]]: """Fan-out following identities according to current follows. Args: follow_configs (list): tuples of (Schema, id) to pull relationships for Response: list: List of emails of identities with follows. """ profile_types, node_types, identifiers, relationships, subscription_names = ( _get_identities_params(follow_configs) ) return subscriptions.get_identities_with_entities_subscriptions( profile_types, node_types, identifiers, relationships, subscription_names ) def _get_identities_params( follow_configs: list[tuple[Type[Schema], int]], ) -> tuple[list[str], list[str], list[int], list[str], list[str]]: """Fan-out following identities according to current follows. Args: follow_configs (list): tuples of (Schema, id) to pull relationships for Response: tuple[list, list, list, list, list]: Lists of profile_types, node_types, identifiers, relationships, and subscription_names. """ profile_types_set: set[str] = set() node_types_set: set[str] = set() identifiers_set: set[int] = set() relationships_set: set[str] = set() subscription_names_set: set[str] = set() for schema_cls, identifier in follow_configs: if not identifier: continue schema = schema_cls() profile_types_set.update(schema.declared_fields['profile_type'].validate.choices) # ty: ignore[unresolved-attribute] node_types_set.add(schema.declared_fields['entity_node_type'].load_default) identifiers_set.add(identifier) relationships_set.add(schema.declared_fields['relationship'].validate.comparable) # ty: ignore[unresolved-attribute] subscription_name_field = schema.declared_fields.get('subscription_name') if subscription_name_field: subscription_names_set.add(subscription_name_field.load_default) return ( list(profile_types_set), list(node_types_set), list(identifiers_set), list(relationships_set), list(subscription_names_set), ) def _build_notifications_delivery_message( subject: str, recipient: str, template: str, variables: dict, ) -> dict[str, Any]: return { 'type': email.NOTIFICATIONS_DELIVERY_MESSAGE_TYPE_GENERAL, 'subject': subject, 'to': [recipient], 'template': template, 'lang': 'en', 'sender': config.SES_SENDER, 'variables': variables, } def _build_statement_period_closed_emails( statement_period: dict[str, Any], active_collaborator_ids: list[int], ) -> list[dict[str, Any]]: """Generate statement period closed messages. Args: statement_period (CollaboratorsStatementPeriod): The statement period that was closed. active_collaborator_ids (List[int]): The collaborators who have activity on the closed period. """ follow_configs: FollowConfigs = [(CollaboratorFollow, x) for x in active_collaborator_ids] identities = _get_following_identities(follow_configs) vendor_id = statement_period['vendor_id'] vendor_name = label.get_name_for_label('Vendor', vendor_id).message default_brand = label.get_default_brand_for_label('Vendor', vendor_id).message messages = [] for ident in identities: tracking_url = _generate_tracking_url(ident['id'], ident['email']) accounting_url = _generate_accounting_url( ident['id'], statement_period['id'], default_brand ) message = _build_notifications_delivery_message( subject=email.COLLABORATORS_STATEMENT_PERIOD_SUBJECT_LINE, recipient=ident['email'], template=email.COLLABORATORS_STATEMENT_PERIOD_TEMPLATE_NAME, variables={ 'statement_period': statement_period, 'vendor_name': vendor_name, 'default_brand': default_brand, 'accounting_url': accounting_url, 'tracking_url': tracking_url, }, ) messages.append(message) if config.SEGMENT_WRITE_KEY: analytics.identify(ident['id']) analytics.track( ident['id'], email.COLLABORATORS_STATEMENT_PERIOD_SEGMENT_EVENT, { 'email': ident['email'], 'vendor_id': statement_period['vendor_id'], 'statement_period_id': statement_period['id'], 'brand': default_brand, }, ) return messages def bulk_statement_period_closed_email(items: list[dict[str, Any]]) -> response.Response: """Send statement period closed messages for many periods. Args: items (list): A list of dicts w/ statement period & collaborator id info. """ all_messages = [] for item in items: statement_period = item['statement_period'] active_collaborator_ids = item.get('active_collaborator_ids', []) messages = _build_statement_period_closed_emails(statement_period, active_collaborator_ids) all_messages.extend(messages) sqs.send_messages(config.NOTIFICATIONS_DELIVERY_URL, all_messages) return response.Response(status=ows_status.OK)