Repository: mozmeao/basket

Issue:
Add new API endpoint for FxA concerts
These data will go directly into an SFMC data extension:

```yaml
name: FxAccounts_Concert_RSVP
fields:
  - Email: EmailAddress
  - Firefox: Text (Y or N)
  - Campaign_ID: Text 100
  - RSVP_Time: DateTime
```


Discussion:
@jpetto FYI. I'll have a PR for this today. You may have to bring it to the finish line if we encounter any hiccups.
Also this means a custom basket integration with bedrock on a successful verification from FxA. I'll get you the details of that in an issue once this is done.

Resolving patch:
diff --git a/basket/news/tasks.py b/basket/news/tasks.py
--- a/basket/news/tasks.py
+++ b/basket/news/tasks.py
@@ -750,6 +750,16 @@ def send_recovery_message_task(email):
     send_message.delay(message_id, email, user_data['id'], token=user_data['token'])
 
 
+@et_task
+def record_fxa_concerts_rsvp(email, is_firefox, campaign_id):
+    sfmc.add_row('FxAccounts_Concert_RSVP', {
+        'Email': email,
+        'Firefox': is_firefox,
+        'Campaign_ID': campaign_id,
+        'RSVP_Time': gmttime(),
+    })
+
+
 @et_task
 def record_source_url(email, source_url, newsletter_id):
     if not source_url:
diff --git a/basket/news/urls.py b/basket/news/urls.py
--- a/basket/news/urls.py
+++ b/basket/news/urls.py
@@ -1,9 +1,9 @@
 from django.conf.urls import url
 
 from .views import (confirm, custom_unsub_reason, debug_user,
-                    fxa_activity, fxa_register, get_involved, list_newsletters, lookup_user,
-                    newsletters, send_recovery_message, subscribe, subscribe_sms, sync_route,
-                    unsubscribe, user, user_meta)
+                    fxa_activity, fxa_concerts_rsvp, fxa_register, get_involved,
+                    list_newsletters, lookup_user, newsletters, send_recovery_message,
+                    subscribe, subscribe_sms, sync_route, unsubscribe, user, user_meta)
 
 
 def token_url(url_prefix, *args, **kwargs):
@@ -16,6 +16,7 @@ def token_url(url_prefix, *args, **kwargs):
     url('^get-involved/$', get_involved),
     url('^fxa-register/$', fxa_register),
     url('^fxa-activity/$', fxa_activity),
+    url('^fxa-concerts-rsvp/$', fxa_concerts_rsvp),
     url('^subscribe/$', subscribe),
     url('^subscribe_sms/$', subscribe_sms),
     token_url('unsubscribe', unsubscribe),
diff --git a/basket/news/views.py b/basket/news/views.py
--- a/basket/news/views.py
+++ b/basket/news/views.py
@@ -24,6 +24,7 @@
     add_fxa_activity,
     add_sms_user,
     confirm_user,
+    record_fxa_concerts_rsvp,
     send_recovery_message_task,
     update_custom_unsub,
     update_fxa_info,
@@ -213,6 +214,34 @@ def fxa_register(request):
     return HttpResponseJSON({'status': 'ok'})
 
 
+@require_POST
+@csrf_exempt
+def fxa_concerts_rsvp(request):
+    if not has_valid_api_key(request):
+        return HttpResponseJSON({
+            'status': 'error',
+            'desc': 'requires a valid API-key',
+            'code': errors.BASKET_AUTH_ERROR,
+        }, 401)
+
+    fields = ('email', 'is_firefox', 'campaign_id')
+    data = request.POST.dict()
+    if not all(f in data for f in fields):
+        return HttpResponseJSON({
+            'status': 'error',
+            'desc': 'missing required field',
+            'code': errors.BASKET_USAGE_ERROR,
+        }, 401)
+
+    is_firefox = 'Y' if data['is_firefox'][0].upper() == 'Y' else 'N'
+    record_fxa_concerts_rsvp.delay(
+        email=data['email'],
+        is_firefox=is_firefox,
+        campaign_id=data['campaign_id'],
+    )
+    return HttpResponseJSON({'status': 'ok'})
+
+
 @require_POST
 @csrf_exempt
 def get_involved(request):


Test patch:
diff --git a/basket/news/tests/test_views.py b/basket/news/tests/test_views.py
--- a/basket/news/tests/test_views.py
+++ b/basket/news/tests/test_views.py
@@ -1087,3 +1087,53 @@ def test_invalid_email(self, ve_mock):
         """Should return None for an invalid email."""
         ve_mock.side_effect = EmailSyntaxError
         self.assertIsNone(utils.process_email(self.email))
+
+
+class FxAConcertsRSVPTests(ViewsPatcherMixin, TestCase):
+    def setUp(self):
+        self.rf = RequestFactory()
+
+        self._patch_views('has_valid_api_key')
+        self._patch_views('record_fxa_concerts_rsvp')
+
+    def test_valid_submission(self):
+        self.has_valid_api_key.return_value = True
+        req = self.rf.post('/', {
+            'email': 'dude@example.com',
+            'is_firefox': 'yes',
+            'campaign_id': 'bowling',
+        })
+        resp = views.fxa_concerts_rsvp(req)
+        self.record_fxa_concerts_rsvp.delay.assert_called_with(
+            email='dude@example.com',
+            is_firefox='Y',
+            campaign_id='bowling',
+        )
+        self.assertEqual(resp.status_code, 200, resp.content)
+        data = json.loads(resp.content)
+        self.assertEqual('ok', data['status'])
+
+    def test_requires_api_key(self):
+        self.has_valid_api_key.return_value = False
+        req = self.rf.post('/', {
+            'email': 'dude@example.com',
+            'is_firefox': 'yes',
+            'campaign_id': 'bowling',
+        })
+        resp = views.fxa_concerts_rsvp(req)
+        self.assertFalse(self.record_fxa_concerts_rsvp.delay.called)
+        self.assertEqual(resp.status_code, 401, resp.content)
+        data = json.loads(resp.content)
+        self.assertEqual(errors.BASKET_AUTH_ERROR, data['code'])
+
+    def test_requires_all_fields(self):
+        self.has_valid_api_key.return_value = True
+        req = self.rf.post('/', {
+            'email': 'dude@example.com',
+            'is_firefox': 'yes',
+        })
+        resp = views.fxa_concerts_rsvp(req)
+        self.assertFalse(self.record_fxa_concerts_rsvp.delay.called)
+        self.assertEqual(resp.status_code, 401, resp.content)
+        data = json.loads(resp.content)
+        self.assertEqual(errors.BASKET_USAGE_ERROR, data['code'])