Repository: mozmeao/basket

Issue:
Include FxA Tips subscription info with user data
Include the FxA tips subscirption status of an email address with requests to `lookup-user`. This data is in data extensions in SFMC. The user must be in the FxA account holders extension, and not in the FxA account opt-out extension. The data returned can have 3 states for this. 

1. Nothing returned for `fxa-tips` means that the user has no record of having an FxA.
2. `True` means the user is an FxA holder, but has no opt-out.
3. `False` means the user is an FxA holder, but has an opt-out.


Discussion:
Please coordinate with Lauren Niolet on the plan surrounding FxA Tips.

_Based on the current plan to my knowledge, we are sunsetting the existing Firefox Accounts Tips subscription, and there will not be any sort of new FxA Tips subscription stood up. See doc below._

https://docs.google.com/spreadsheets/d/1CdsuwXBhXhJ9LBMthRMDx9ATEGWDiKXlZoPFWLjjKzI/edit#gid=0
Right. I was trying to refer to the FxA account holders emails that currently come only from SFMC. That's what this issue is about. The current newsletter should be unaffected by this work.
Updates: We will not need to indicate whether the user has opted out of these emails because the opt-out mechanism is being removed and opt-out will now only be possible via FxA deletion. So this will only need to return something like `'fxa-user': True` or `False`.
We should also make inclusion of that data optional via a query param to these requests. We'll sometimes be able to know if they're an FxA user via other means, so there will be no need to take the time to look them up in SFMC in those cases.

Resolving patch:
diff --git a/basket/news/tasks.py b/basket/news/tasks.py
--- a/basket/news/tasks.py
+++ b/basket/news/tasks.py
@@ -272,6 +272,8 @@ def fxa_verified(data):
             'newsletters': settings.FXA_REGISTER_NEWSLETTER,
             'source_url': fxa_source_url(metrics),
         })
+    else:
+        record_source_url(email, fxa_source_url(metrics), 'fxa-no-optin')
 
 
 @et_task
diff --git a/basket/news/utils.py b/basket/news/utils.py
--- a/basket/news/utils.py
+++ b/basket/news/utils.py
@@ -23,8 +23,9 @@
 from email_validator import validate_email, EmailNotValidError
 from raven.contrib.django.raven_compat.models import client as sentry_client
 
-from basket.news.backends.common import NewsletterException
+from basket.news.backends.common import NewsletterException, NewsletterNoResultsException
 from basket.news.backends.sfdc import sfdc
+from basket.news.backends.sfmc import sfmc
 from basket.news.models import APIUser, BlockedEmail
 from basket.news.newsletters import newsletter_inactive_slugs, newsletter_group_newsletter_slugs, \
     newsletter_languages
@@ -274,7 +275,7 @@ def language_code_is_valid(code):
 ]
 
 
-def get_user_data(token=None, email=None, extra_fields=None):
+def get_user_data(token=None, email=None, extra_fields=None, get_fxa=False):
     """Return a dictionary of the user's data from Exact Target.
     Look them up by their email if given, otherwise by the token.
 
@@ -285,6 +286,10 @@ def get_user_data(token=None, email=None, extra_fields=None):
     call this function with said field name in a list passed in
     the `extra_fields` argument.
 
+    If `get_fxa` is True then the email will be looked up in the FxA Data Extension
+    in SFMC and a boolean field will be including indicating whether they are
+    an account holder or not.
+
     Review of results:
 
     None = user completely unknown, no errors talking to ET.
@@ -325,11 +330,24 @@ def get_user_data(token=None, email=None, extra_fields=None):
         if extra_fields and fn not in extra_fields:
             user.pop(fn, None)
 
+    if get_fxa:
+        try:
+            sfmc.get_row('Firefox_Account_ID', ['FXA_ID'], email=user['email'])
+        except NewsletterNoResultsException:
+            fxa_user = False
+        except NewsletterException:
+            fxa_user = None
+        else:
+            fxa_user = True
+
+        if fxa_user is not None:
+            user['has_fxa'] = fxa_user
+
     user['status'] = 'ok'
     return user
 
 
-def get_user(token=None, email=None):
+def get_user(token=None, email=None, get_fxa=False):
     if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY:
         # can't return user data during maintenance
         return HttpResponseJSON({
@@ -339,7 +357,7 @@ def get_user(token=None, email=None):
         }, 400)
 
     try:
-        user_data = get_user_data(token, email)
+        user_data = get_user_data(token, email, get_fxa=get_fxa)
         status_code = 200
     except NewsletterException as e:
         return newsletter_exception_response(e)
diff --git a/basket/news/views.py b/basket/news/views.py
--- a/basket/news/views.py
+++ b/basket/news/views.py
@@ -550,7 +550,8 @@ def user(request, token):
             data['email'] = email
         return update_user_task(request, SET, data)
 
-    return get_user(token)
+    get_fxa = 'fxa' in request.GET
+    return get_user(token, get_fxa=get_fxa)
 
 
 @require_POST
@@ -685,6 +686,7 @@ def lookup_user(request):
 
     token = request.GET.get('token', None)
     email = request.GET.get('email', None)
+    get_fxa = 'fxa' in request.GET
 
     if (not email and not token) or (email and token):
         return HttpResponseJSON({
@@ -707,7 +709,7 @@ def lookup_user(request):
             return invalid_email_response()
 
     try:
-        user_data = get_user_data(token=token, email=email)
+        user_data = get_user_data(token=token, email=email, get_fxa=get_fxa)
     except NewsletterException as e:
         return newsletter_exception_response(e)
 


Test patch:
diff --git a/basket/news/tests/test_tasks.py b/basket/news/tests/test_tasks.py
--- a/basket/news/tests/test_tasks.py
+++ b/basket/news/tests/test_tasks.py
@@ -798,7 +798,8 @@ def test_firefox_ios_tablet(self):
 @patch('basket.news.tasks._update_fxa_info')
 @patch('basket.news.tasks.upsert_user')
 class FxAVerifiedTests(TestCase):
-    def test_no_subscribe(self, upsert_mock, fxa_info_mock):
+    @patch('basket.news.tasks.sfmc')
+    def test_no_subscribe(self, sfmc_mock, upsert_mock, fxa_info_mock):
         data = {
             'email': 'thedude@example.com',
             'uid': 'the-fxa-id',
@@ -847,7 +848,8 @@ def test_with_subscribe_and_metrics(self, upsert_mock, fxa_info_mock):
         })
         fxa_info_mock.assert_called_with(data['email'], 'en-US', data['uid'], 'monitor', None)
 
-    def test_with_createDate(self, upsert_mock, fxa_info_mock):
+    @patch('basket.news.tasks.sfmc')
+    def test_with_createDate(self, sfmc_mock, upsert_mock, fxa_info_mock):
         create_date_float = 1526996035.498
         create_date = datetime.fromtimestamp(create_date_float)
         data = {
diff --git a/basket/news/tests/test_users.py b/basket/news/tests/test_users.py
--- a/basket/news/tests/test_users.py
+++ b/basket/news/tests/test_users.py
@@ -10,7 +10,7 @@
 from basket import errors
 
 from basket.news import views
-from basket.news.backends.common import NewsletterException
+from basket.news.backends.common import NewsletterException, NewsletterNoResultsException
 from basket.news.models import APIUser
 from basket.news.utils import SET, generate_token
 
@@ -74,16 +74,73 @@ def test_both_parms(self):
         rsp = self.get(params=params)
         self.assertEqual(400, rsp.status_code, rsp.content)
 
-    @patch('basket.news.views.get_user_data')
-    def test_with_token(self, get_user_data):
+    @patch('basket.news.utils.sfdc')
+    @patch('basket.news.utils.sfmc')
+    def test_with_token(self, sfmc_mock, sfdc_mock):
         """Passing a token gets back that user's data"""
-        get_user_data.return_value = self.user_data
+        sfdc_mock.get.return_value = self.user_data
         params = {
             'token': 'dummy',
         }
         rsp = self.get(params=params)
         self.assertEqual(200, rsp.status_code, rsp.content)
         self.assertEqual(self.user_data, json.loads(rsp.content))
+        sfmc_mock.get_row.assert_not_called()
+
+    @patch('basket.news.utils.sfdc')
+    @patch('basket.news.utils.sfmc')
+    def test_get_fxa_status(self, sfmc_mock, sfdc_mock):
+        """Should request FxA status from SFMC"""
+        user_data = self.user_data.copy()
+        user_data['email'] = 'hisdudeness@example.com'
+        sfdc_mock.get.return_value = user_data
+        response = user_data.copy()
+        response['has_fxa'] = True
+        params = {
+            'token': 'dummy',
+            'fxa': '1',
+        }
+        rsp = self.get(params=params)
+        sfmc_mock.get_row.assert_called_with('Firefox_Account_ID', ['FXA_ID'], email=user_data['email'])
+        self.assertEqual(200, rsp.status_code, rsp.content)
+        self.assertEqual(response, json.loads(rsp.content))
+
+    @patch('basket.news.utils.sfdc')
+    @patch('basket.news.utils.sfmc')
+    def test_get_fxa_status_false(self, sfmc_mock, sfdc_mock):
+        """Should request FxA status from SFMC"""
+        user_data = self.user_data.copy()
+        user_data['email'] = 'hisdudeness@example.com'
+        sfdc_mock.get.return_value = user_data
+        response = user_data.copy()
+        response['has_fxa'] = False
+        sfmc_mock.get_row.side_effect = NewsletterNoResultsException
+        params = {
+            'token': 'dummy',
+            'fxa': '1',
+        }
+        rsp = self.get(params=params)
+        sfmc_mock.get_row.assert_called_with('Firefox_Account_ID', ['FXA_ID'], email=user_data['email'])
+        self.assertEqual(200, rsp.status_code, rsp.content)
+        self.assertEqual(response, json.loads(rsp.content))
+
+    @patch('basket.news.utils.sfdc')
+    @patch('basket.news.utils.sfmc')
+    def test_get_fxa_status_error(self, sfmc_mock, sfdc_mock):
+        """Should request FxA status from SFMC"""
+        user_data = self.user_data.copy()
+        user_data['email'] = 'hisdudeness@example.com'
+        sfdc_mock.get.return_value = user_data
+        sfmc_mock.get_row.side_effect = NewsletterException
+        params = {
+            'token': 'dummy',
+            'fxa': '1',
+        }
+        rsp = self.get(params=params)
+        sfmc_mock.get_row.assert_called_with('Firefox_Account_ID', ['FXA_ID'], email=user_data['email'])
+        self.assertEqual(200, rsp.status_code, rsp.content)
+        # should not contain 'has_fxa' at all on general error
+        self.assertEqual(user_data, json.loads(rsp.content))
 
     def test_with_email_no_api_key(self):
         """Passing email without api key is a 401"""