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

Commit

Permalink
feat: add new task for sending offer usage email via braze api-trigge…
Browse files Browse the repository at this point in the history
…red campaign.

This reverts commit 3bb32af.
  • Loading branch information
iloveagent57 committed Jul 5, 2022
1 parent ebcb8cb commit fe4928d
Show file tree
Hide file tree
Showing 17 changed files with 577 additions and 170 deletions.
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
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@ clean: ## delete generated byte code and coverage reports
coverage erase
rm -rf cover htmlcov

COMMON_CONSTRAINTS_TXT=requirements/common_constraints.txt
.PHONY: $(COMMON_CONSTRAINTS_TXT)
$(COMMON_CONSTRAINTS_TXT):
wget -O "$(@)" https://raw.githubusercontent.com/edx/edx-lint/master/edx_lint/files/common_constraints.txt || touch "$(@)"

export CUSTOM_COMPILE_COMMAND = make upgrade
upgrade: ## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
pip3 install -q -r requirements/pip_tools.txt
upgrade: $(COMMON_CONSTRAINTS_TXT)
## update the requirements/*.txt files with the latest packages satisfying requirements/*.in
pip install -q -r requirements/pip_tools.txt
pip-compile --allow-unsafe --rebuild --upgrade -o requirements/pip.txt requirements/pip.in
pip-compile --rebuild --upgrade -o requirements/pip_tools.txt requirements/pip_tools.in
pip install -q -r requirements/pip.txt
pip install -q -r requirements/pip_tools.txt
pip-compile --upgrade -o requirements/tox.txt requirements/tox.in
pip-compile --upgrade -o requirements/base.txt requirements/base.in
pip-compile --upgrade -o requirements/test.txt requirements/test.in
Expand Down
8 changes: 4 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
⛔️ DEPRECATION WARNING
======================
DEPRECATION WARNING
====================
This repository is deprecated and in maintainence-only operation while we work on a replacement, please see `this announcement <https://discuss.openedx.org/t/deprecation-removal-ecommerce-service-depr-22/6839>`__ for more information.
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Although we have stopped integrating new contributions, we always appreciate security disclosures and patches sent to `[email protected] <mailto:[email protected]>`__

edX Ecommerce Worker |Build|_ |Codecov|_
==========================================
=========================================
.. |Build| image:: https://github.com/edx/ecommerce-worker/workflows/Python%20CI/badge.svg?branch=master
.. _Build: https://github.com/edx/ecommerce-worker/actions?query=workflow%3A%22Python+CI%22

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 {},
}
67 changes: 65 additions & 2 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 @@ -107,8 +117,11 @@ 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):
"""
Sends the offer usage email via braze.
DEPRECATED: This function will eventually be removed in favor of
``send_offer_usage_email_via_braze_api_triggered_campaign`` below.
See https://2u-internal.atlassian.net/browse/ENT-5940
Sends the offer usage email via braze.
Args:
self: Ignore.
emails (str): comma separated emails.
Expand Down Expand Up @@ -142,6 +155,56 @@ def send_offer_usage_email_via_braze(self, emails, subject, email_body, reply_to
)


def send_api_triggered_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.
lms_user_ids_by_email (dict): Map of recipient email addresses to LMS user ids.
subject (str): Email subject.
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:
braze_client = EdxBrazeClient(site_code)

message_kwargs = {
'campaign_id': campaign_id or config.get('ENTERPRISE_CODE_USAGE_API_TRIGGERED_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_variables)
)
raise


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

0 comments on commit fe4928d

Please sign in to comment.