Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Enterprise offer usage email updates #170

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ target/
# Vim
*.swp

# Emacs
*~

# Local configuration overrides
private.py

Expand Down
12 changes: 7 additions & 5 deletions ecommerce_worker/configuration/devstack.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
from logging.config import dictConfig
import os

import yaml

Expand All @@ -15,12 +16,13 @@
dictConfig(logger_config)
# END LOGGING

filename = get_overrides_filename('ECOMMERCE_WORKER_CFG')
with open(filename) as f:
config_from_yaml = yaml.load(f)
if not os.environ.get('IGNORE_YAML_OVERRIDES'):
filename = get_overrides_filename('ECOMMERCE_WORKER_CFG')
with open(filename) as f:
config_from_yaml = yaml.load(f)

# Override base configuration with values from disk.
vars().update(config_from_yaml)
# Override base configuration with values from disk.
vars().update(config_from_yaml)

# Apply any developer-defined overrides.
try:
Expand Down
121 changes: 85 additions & 36 deletions ecommerce_worker/email/v1/braze/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import requests
from celery.utils.log import get_task_logger

from braze import client as edx_braze_client

from ecommerce_worker.email.v1.braze.exceptions import (
ConfigurationError,
BrazeNotEnabled,
Expand All @@ -22,7 +24,7 @@
log = get_task_logger(__name__)


def is_braze_enabled(site_code) -> bool: # pylint: disable=missing-function-docstring
def is_braze_enabled(site_code) -> bool:
config = get_braze_configuration(site_code)
return bool(config.get('BRAZE_ENABLE'))

Expand All @@ -38,6 +40,23 @@ def get_braze_configuration(site_code):
return config


def validate_braze_config(config, site_code):
"""
Raises if braze is not enabled or if either the
Rest or Webapp API keys are missing from the configuration.
"""
# Return if Braze integration disabled
if not config.get('BRAZE_ENABLE'):
msg = f'Braze is not enabled for site {site_code}'
log.error(msg)
raise BrazeNotEnabled(msg)

if not (config.get('BRAZE_REST_API_KEY') and config.get('BRAZE_WEBAPP_API_KEY')):
msg = f'Required keys missing for site {site_code}'
log.error(msg)
raise ConfigurationError(msg)


def get_braze_client(site_code):
"""
Returns a Braze client for the specified site.
Expand All @@ -52,44 +71,21 @@ def get_braze_client(site_code):
BrazeNotEnabled: If Braze is not enabled for the specified site.
ConfigurationError: If either the Braze API key or Webapp key are not set for the site.
"""
# Get configuration
config = get_braze_configuration(site_code)

# Return if Braze integration disabled
if not config.get('BRAZE_ENABLE'):
msg = f'Braze is not enabled for site {site_code}'
log.debug(msg)
raise BrazeNotEnabled(msg)

rest_api_key = config.get('BRAZE_REST_API_KEY')
webapp_api_key = config.get('BRAZE_WEBAPP_API_KEY')
rest_api_url = config.get('REST_API_URL')
messages_send_endpoint = config.get('MESSAGES_SEND_ENDPOINT')
email_bounce_endpoint = config.get('EMAIL_BOUNCE_ENDPOINT')
new_alias_endpoint = config.get('NEW_ALIAS_ENDPOINT')
users_track_endpoint = config.get('USERS_TRACK_ENDPOINT')
export_id_endpoint = config.get('EXPORT_ID_ENDPOINT')
campaign_send_endpoint = config.get('CAMPAIGN_SEND_ENDPOINT')
enterprise_campaign_id = config.get('ENTERPRISE_CAMPAIGN_ID')
from_email = config.get('FROM_EMAIL')

if not rest_api_key or not webapp_api_key:
msg = f'Required keys missing for site {site_code}'
log.error(msg)
raise ConfigurationError(msg)
validate_braze_config(config, site_code)

return BrazeClient(
rest_api_key=rest_api_key,
webapp_api_key=webapp_api_key,
rest_api_url=rest_api_url,
messages_send_endpoint=messages_send_endpoint,
email_bounce_endpoint=email_bounce_endpoint,
new_alias_endpoint=new_alias_endpoint,
users_track_endpoint=users_track_endpoint,
export_id_endpoint=export_id_endpoint,
campaign_send_endpoint=campaign_send_endpoint,
enterprise_campaign_id=enterprise_campaign_id,
from_email=from_email,
rest_api_key=config.get('BRAZE_REST_API_KEY'),
webapp_api_key=config.get('BRAZE_WEBAPP_API_KEY'),
rest_api_url=config.get('REST_API_URL'),
messages_send_endpoint=config.get('MESSAGES_SEND_ENDPOINT'),
email_bounce_endpoint=config.get('EMAIL_BOUNCE_ENDPOINT'),
new_alias_endpoint=config.get('NEW_ALIAS_ENDPOINT'),
users_track_endpoint=config.get('USERS_TRACK_ENDPOINT'),
export_id_endpoint=config.get('EXPORT_ID_ENDPOINT'),
campaign_send_endpoint=config.get('CAMPAIGN_SEND_ENDPOINT'),
enterprise_campaign_id=config.get('ENTERPRISE_CAMPAIGN_ID'),
from_email=config.get('FROM_EMAIL'),
)


Expand Down Expand Up @@ -526,3 +522,56 @@ def get_braze_external_id(
return response["users"][0]["external_id"]

return None


class EdxBrazeClient(edx_braze_client.BrazeClient):
"""
Wrapper around the edx-braze-client library BrazeClient class.
TODO: Deprecate ``BrazeClient`` above and use only this class
for Braze interactions.
"""
def __init__(self, site_code):
config = get_braze_configuration(site_code)
validate_braze_config(config, site_code)

super().__init__(
api_key=config.get('BRAZE_REST_API_KEY'),
api_url=config.get('REST_API_URL'),
app_id=config.get('BRAZE_WEBAPP_API_KEY'),
)

def create_recipient(
self,
user_email,
lms_user_id,
trigger_properties=None,
):
"""
Create a recipient object using the given user_email and lms_user_id.
"""

user_alias = {
'alias_label': 'Enterprise',
'alias_name': user_email,
}

# Identify the user alias in case it already exists. This is necessary so
# we don't accidently create a duplicate Braze profile.
self.identify_users([{
'external_id': lms_user_id,
'user_alias': user_alias,
}])

attributes = {
"user_alias": user_alias,
"email": user_email,
"_update_existing_only": False,
}

return {
'external_user_id': lms_user_id,
'attributes': attributes,
# If a profile does not already exist, Braze will create a new profile before sending a message.
'send_to_existing_only': False,
'trigger_properties': trigger_properties or {},
}
69 changes: 46 additions & 23 deletions ecommerce_worker/email/v1/braze/tasks.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
"""
This file contains celery task functionality for braze.
"""
from operator import itemgetter

import braze.exceptions as edx_braze_exceptions
from celery.utils.log import get_task_logger

from ecommerce_worker.email.v1.braze.client import get_braze_client, get_braze_configuration
from ecommerce_worker.email.v1.braze.client import (
get_braze_client,
get_braze_configuration,
EdxBrazeClient,
)
from ecommerce_worker.email.v1.braze.exceptions import BrazeError, BrazeRateLimitError, BrazeInternalServerError
from ecommerce_worker.email.v1.utils import update_assignment_email_status

logger = get_task_logger(__name__)

# Use a smaller countdown for the offer usage task,
# since the mgmt command that executes it blocks until the task is done/failed.
OFFER_USAGE_RETRY_DELAY_SECONDS = 10


def send_offer_assignment_email_via_braze(self, user_email, offer_assignment_id, subject, email_body, sender_alias,
reply_to, attachments, site_code):
Expand Down Expand Up @@ -105,41 +115,54 @@ def send_offer_update_email_via_braze(self, user_email, subject, email_body, sen
)


def send_offer_usage_email_via_braze(self, emails, subject, email_body, reply_to, attachments, site_code):
def send_offer_usage_email_via_braze(
self, lms_user_ids_by_email, subject, email_body_variables, site_code, campaign_id=None
):
"""
Sends the offer usage email via braze.

Args:
self: Ignore.
emails (str): comma separated emails.
lms_user_ids_by_email (dict): Map of recipient email addresses to LMS user ids.
subject (str): Email subject.
email_body (str): The body of the email.
reply_to (str): Enterprise Customer reply to address for email reply.
attachments (list): File attachment list with dicts having 'file_name' and 'url' keys.
email_body_variables (dict): key-value pairs that are injected into Braze email template for personalization.
site_code (str): Identifier of the site sending the email.
campaign_id (str): Identifier of Braze API-triggered campaign to send message through; defaults
to config.ENTERPRISE_CODE_USAGE_CAMPAIGN_ID
"""
config = get_braze_configuration(site_code)
try:
user_emails = list(emails.strip().split(","))
braze_client = get_braze_client(site_code)
_send_braze_message(
braze_client,
email_ids=user_emails,
subject=subject,
body=email_body,
reply_to=reply_to,
attachments=attachments,
campaign_id=config.get('ENTERPRISE_CODE_USAGE_CAMPAIGN_ID'),
message_variation_id=config.get('ENTERPRISE_CODE_USAGE_MESSAGE_VARIATION_ID'),
)
except (BrazeRateLimitError, BrazeInternalServerError) as exc:
raise self.retry(countdown=config.get('BRAZE_RETRY_SECONDS'),
max_retries=config.get('BRAZE_RETRY_ATTEMPTS')) from exc
except BrazeError:
braze_client = EdxBrazeClient(site_code)

message_kwargs = {
'campaign_id': campaign_id or config.get('ENTERPRISE_CODE_USAGE_CAMPAIGN_ID'),
'recipients': [],
'emails': [],
'trigger_properties': {
'subject': subject,
**email_body_variables,
},
}

for user_email, lms_user_id in sorted(lms_user_ids_by_email.items(), key=itemgetter(0)):
if lms_user_id:
recipient = braze_client.create_recipient(user_email, lms_user_id)
message_kwargs['recipients'].append(recipient)
else:
message_kwargs['emails'].append(user_email)

braze_client.send_campaign_message(**message_kwargs)
except (edx_braze_exceptions.BrazeRateLimitError, edx_braze_exceptions.BrazeInternalServerError) as exc:
raise self.retry(
countdown=OFFER_USAGE_RETRY_DELAY_SECONDS,
max_retries=config.get('BRAZE_RETRY_ATTEMPTS')
) from exc
except edx_braze_exceptions.BrazeError:
logger.exception(
'[Offer Usage] Error in offer usage notification with message --- '
'{message}'.format(message=email_body)
'{message}'.format(message=email_body_variables)
)
raise


def send_code_assignment_nudge_email_via_braze(self, email, subject, email_body, sender_alias, reply_to, # pylint: disable=invalid-name
Expand Down
Loading