def fxa_email_changed(data): ts = data["ts"] fxa_id = data["uid"] email = data["email"] cache_key = "fxa_email_changed:%s" % fxa_id prev_ts = float(cache.get(cache_key, 0)) if prev_ts and prev_ts > ts: # message older than our last update for this UID return # Update SFDC user_data = get_user_data(fxa_id=fxa_id, extra_fields=["id"]) if user_data: sfdc.update(user_data, {"fxa_primary_email": email}) else: # FxA record not found, try email user_data = get_user_data(email=email, extra_fields=["id"]) if user_data: sfdc.update(user_data, {"fxa_id": fxa_id, "fxa_primary_email": email}) else: # No matching record for Email or FxA ID. Create one. sfdc.add({"email": email, "fxa_id": fxa_id, "fxa_primary_email": email}) statsd.incr("news.tasks.fxa_email_changed.user_not_found") sfmc.upsert_row("FXA_EmailUpdated", {"FXA_ID": fxa_id, "NewEmailAddress": email}) cache.set(cache_key, ts, 7200) # 2 hr
def process_petition_signature(data): """ Add petition signature to SFDC """ data = data['form'] get_lock(data['email']) # tells the backend to leave the "subscriber" flag alone contact_data = {'_set_subscriber': False} contact_data.update( {k: data[k] for k in PETITION_CONTACT_FIELDS if data.get(k)}) user_data = get_user_data(email=data['email'], extra_fields=['id']) if user_data: sfdc.update(user_data, contact_data) else: contact_data['token'] = generate_token() contact_data['email'] = data['email'] contact_data['record_type'] = settings.DONATE_CONTACT_RECORD_TYPE sfdc.add(contact_data) # fetch again to get ID user_data = get_user_data(email=data.get('email'), extra_fields=['id']) if not user_data: # retry here to make sure we associate the donation data with the proper account raise RetryTask('User not yet available') if data.get('email_subscription', False): upsert_user.delay( SUBSCRIBE, { 'token': user_data['token'], 'lang': data.get('lang', 'en-US'), 'newsletters': 'mozilla-foundation', 'source_url': data['source_url'], }) campaign_member = { 'CampaignId': data['campaign_id'], 'ContactId': user_data['id'], 'Full_URL__c': data['source_url'], 'Status': 'Signed', } comments = data.get('comments') if comments: campaign_member['Petition_Comments__c'] = comments[:500] metadata = data.get('metadata') if metadata: campaign_member['Petition_Flex__c'] = json.dumps(metadata)[:500] try: sfdc.campaign_member.create(campaign_member) except sfapi.SalesforceMalformedRequest as e: if e.content and e.content[0].get('errorCode') == 'DUPLICATE_VALUE': # already in the system, ignore pass else: raise
def process_petition_signature(data): """ Add petition signature to SFDC """ data = data['form'] get_lock(data['email']) # tells the backend to leave the "subscriber" flag alone contact_data = {'_set_subscriber': False} contact_data.update({k: data[k] for k in PETITION_CONTACT_FIELDS if data.get(k)}) user_data = get_user_data(email=data['email'], extra_fields=['id']) if user_data: sfdc.update(user_data, contact_data) else: contact_data['token'] = generate_token() contact_data['email'] = data['email'] contact_data['record_type'] = settings.DONATE_CONTACT_RECORD_TYPE sfdc.add(contact_data) # fetch again to get ID user_data = get_user_data(email=data.get('email'), extra_fields=['id']) if not user_data: # retry here to make sure we associate the donation data with the proper account raise RetryTask('User not yet available') if data.get('email_subscription', False): upsert_user.delay(SUBSCRIBE, { 'token': user_data['token'], 'lang': 'en-US', 'newsletters': 'mozilla-foundation', 'source_url': data['source_url'], }) campaign_member = { 'CampaignId': data['campaign_id'], 'ContactId': user_data['id'], 'Full_URL__c': data['source_url'], 'Status': 'Signed', } comments = data.get('comments') if comments: campaign_member['Petition_Comments__c'] = comments[:500] metadata = data.get('metadata') if metadata: campaign_member['Petition_Flex__c'] = json.dumps(metadata)[:500] try: sfdc.campaign_member.create(campaign_member) except sfapi.SalesforceMalformedRequest as e: if e.content and e.content[0].get('errorCode') == 'DUPLICATE_VALUE': # already in the system, ignore pass else: raise
def sfdc_add_update(update_data, user_data=None): # for use with maintenance mode only # TODO remove after maintenance is over and queue is processed if user_data: sfdc.update(user_data, update_data) ctms.update(user_data, update_data) else: ctms_data = update_data.copy() ctms_contact = ctms.add(ctms_data) if ctms_contact: update_data["email_id"] = ctms_contact["email"]["email_id"] try: sfdc.add(update_data) except sfapi.SalesforceMalformedRequest as e: # noqa # possibly a duplicate email. try the update below. user_data = get_user_data(email=update_data["email"], extra_fields=["id"]) if user_data: # we have a user, delete generated token and email_id # and continue with an update update_data.pop("token", None) update_data.pop("email_id", None) sfdc.update(user_data, update_data) ctms.update(user_data, update_data) else: # still no user, try the add one more time ctms_contact = ctms.add(update_data) if ctms_contact: update_data["email_id"] = ctms_contact["email"]["email_id"] sfdc.add(update_data)
def confirm_user(token): """ Confirm any pending subscriptions for the user with this token. If any of the subscribed newsletters have welcome messages, send them. :param token: User's token :param user_data: Dictionary with user's data from Exact Target, as returned by get_user_data(), or None if that wasn't available when this was called. :raises: BasketError for fatal errors, NewsletterException for retryable errors. """ get_lock(token) user_data = get_user_data(token=token) if user_data is None: statsd.incr('news.tasks.confirm_user.confirm_user_not_found') return if user_data['optin']: # already confirmed return if not ('email' in user_data and user_data['email']): raise BasketError('token has no email in ET') sfdc.update(user_data, {'optin': True})
def process_subhub_event_customer_created(data): """ Event name: customer.created Creates or updates a SFDC customer when a new payment processor/Stripe customer is created """ statsd.incr('news.tasks.process_subhub_event.customer_created') first, last = split_name(data['name']) contact_data = {'fxa_id': data['user_id'], 'payee_id': data['customer_id']} user_data = get_user_data(email=data['email']) # if user was found in sfdc, see if we should update their name(s) if user_data: # if current last name is '_', update it if user_data['last_name'] == '_': contact_data['last_name'] = last # if current last name is blank/Null, update it if not user_data['first_name']: contact_data['first_name'] = first sfdc.update(user_data, contact_data) statsd.incr('news.tasks.process_subhub_event.customer_created.updated') # if no user was found, create new user in sfdc else: contact_data['email'] = data['email'] contact_data['first_name'] = first contact_data['last_name'] = last # create the user in sfdc statsd.incr('news.tasks.process_subhub_event.customer_created.created') sfdc.add(contact_data)
def send_recovery_message(request): """ Send a recovery message to an email address. required form parameter: email If email not provided or not syntactically correct, returns 400. If email not known, returns 404. Otherwise, queues a task to send the message and returns 200. """ email = process_email(request.POST.get("email")) if not email: return invalid_email_response() if email_is_blocked(email): # don't let on there's a problem return HttpResponseJSON({"status": "ok"}) try: user_data = get_user_data(email=email) except NewsletterException as e: return newsletter_exception_response(e) if not user_data: return HttpResponseJSON( { "status": "error", "desc": "Email address not known", "code": errors.BASKET_UNKNOWN_EMAIL, }, 404, ) # Note: Bedrock looks for this 404 send_recovery_message_task.delay(email) return HttpResponseJSON({"status": "ok"})
def send_recovery_message(request): """ Send a recovery message to an email address. required form parameter: email If email not provided or not syntactically correct, returns 400. If email not known, returns 404. Otherwise, queues a task to send the message and returns 200. """ email = process_email(request.POST.get('email')) if not email: return invalid_email_response() if email_is_blocked(email): # don't let on there's a problem return HttpResponseJSON({'status': 'ok'}) try: user_data = get_user_data(email=email) except NewsletterException as e: return newsletter_exception_response(e) if not user_data: return HttpResponseJSON({ 'status': 'error', 'desc': 'Email address not known', 'code': errors.BASKET_UNKNOWN_EMAIL, }, 404) # Note: Bedrock looks for this 404 send_recovery_message_task.delay(email) return HttpResponseJSON({'status': 'ok'})
def upsert_amo_user_data(data, user_sync=False): """ Update AMO user data in the SFDC contact, or create a contact. Return the Contact data (the contact ID at a minimum). :param data: dict of amo user data :param user_sync: bool True if this is a User Sync request :return: dict of SFDC contact data """ data = data.copy() fxa_id = data.pop("fxa_id", None) amo_id = data.pop("id", None) user = None # records can come in with no "id" or "fxa_id" field if amo_id: user = get_user_data(amo_id=amo_id, extra_fields=["id", "amo_id", "fxa_id"]) if not user and fxa_id: # Try to find user with fxa_id user = get_user_data(fxa_id=fxa_id, extra_fields=["id", "amo_id", "fxa_id"]) if not user: # Cannot find user with FxA ID or AMO ID, ignore the update return None if user_sync and not user["amo_id"]: # do not update user as AMO User unless it comes from an AddonSync return None amo_deleted = data.pop("deleted", False) amo_data = {f"amo_{k}": v for k, v in data.items() if v} amo_data["amo_id"] = amo_id amo_data["amo_deleted"] = amo_deleted if not user_sync: # only ensure this is true if this is from an addon sync amo_data["amo_user"] = True if amo_deleted or fxa_id is None: amo_data["amo_id"] = None sfdc.update(user, amo_data) ctms.update(user, amo_data) return user
def fxa_email_changed(data): ts = data["ts"] fxa_id = data["uid"] email = data["email"] cache_key = "fxa_email_changed:%s" % fxa_id prev_ts = float(cache.get(cache_key, 0)) if prev_ts and prev_ts > ts: # message older than our last update for this UID return # Update SFDC / CTMS user_data = get_user_data(fxa_id=fxa_id, extra_fields=["id"]) if user_data: sfdc.update(user_data, {"fxa_primary_email": email}) ctms.update(user_data, {"fxa_primary_email": email}) else: # FxA record not found, try email user_data = get_user_data(email=email, extra_fields=["id"]) if user_data: sfdc.update(user_data, { "fxa_id": fxa_id, "fxa_primary_email": email }) ctms.update(user_data, { "fxa_id": fxa_id, "fxa_primary_email": email }) else: # No matching record for Email or FxA ID. Create one. data = { "email": email, "token": generate_token(), "fxa_id": fxa_id, "fxa_primary_email": email, } ctms_data = data.copy() contact = ctms.add(ctms_data) if contact: data["email_id"] = contact["email"]["email_id"] sfdc.add(data) statsd.incr("news.tasks.fxa_email_changed.user_not_found") cache.set(cache_key, ts, 7200) # 2 hr
def get_fxa_user_data(fxa_id, email): """Return a user data dict, just like `get_user_data` below, but ensure we have a good FxA contact First look for a user by FxA ID. If we get a user, and the email matches what was passed in, return it. If the email doesn't match, set the first user's FxA_ID to "DUPE:<fxa_id>" so that we don't run into dupe issues, and set "fxa_deleted" to True. Then look up a user with the email address and return that or None. """ user_data = None # try getting user data with the fxa_id first user_data_fxa = get_user_data(fxa_id=fxa_id, extra_fields=["id"]) if user_data_fxa: user_data = user_data_fxa # If email doesn't match, update FxA primary email field with the new email. if user_data_fxa["email"] != email: sfdc.update(user_data_fxa, {"fxa_primary_email": email}) # if we still don't have user data try again with email this time if not user_data: user_data = get_user_data(email=email, extra_fields=["id"]) return user_data
def upsert_amo_user_data(data): """ Update AMO user data in the SFDC contact, or create a contact. Return the Contact data (the contact ID at a minimum). :param data: dict of amo user data :return: dict of SFDC contact data """ fxa_id = data.pop("fxa_id") amo_id = data.pop("id") user = get_user_data(amo_id=amo_id, extra_fields=["id", "amo_id", "fxa_id"]) if not user: # Try to find user with fxa_id user = get_user_data(fxa_id=fxa_id, extra_fields=["id", "amo_id", "fxa_id"]) if not user: # Cannot find user with FxA ID or AMO ID, ignore the update return None amo_deleted = data.pop("deleted", False) amo_data = {f"amo_{k}": v for k, v in data.items() if v} amo_data["amo_user"] = True amo_data["amo_id"] = amo_id amo_data["amo_deleted"] = amo_deleted if amo_deleted: if fxa_id is None: # Deleted user, we don't want do keep linking that AMO account with # that FxA account in Salesforce, they might re-create a new AMO # account from their previous FxA account. amo_data["amo_id"] = None else: if fxa_id is None: # User takeover protection, we don't want do keep linking that AMO # account with that FxA account in Salesforce amo_data["amo_id"] = None sfdc.update(user, amo_data) return user
def process_subhub_event_subscription_updated(data): statsd.incr('news.tasks.process_subhub_event.subscription_updated') user_data = get_user_data(payee_id=data['customer_id'], extra_fields=['id']) if not user_data: statsd.incr( 'news.tasks.process_subhub_event.subscription_updated.user_not_found' ) raise RetryTask('Could not find user. Try again.') direction = 'Down' if data['event_type'].endswith('downgrade') else 'Up' stage_name = f'Subscription {direction}grade' sfdc.opportunity.create({ 'Amount': cents_to_dollars(data['plan_amount_new']), 'Plan_Amount_Old__c': cents_to_dollars(data['plan_amount_old']), 'Billing_Cycle_End__c': iso_format_unix_timestamp(data['current_period_end']), 'CloseDate': iso_format_unix_timestamp(data.get('close_date', time())), 'Donation_Contact__c': user_data['id'], 'Event_Id__c': data['event_id'], 'Event_Name__c': data['event_type'], 'Invoice_Number__c': data['invoice_number'], 'Name': 'Subscription Services', 'Payment_Interval__c': data['interval'], 'Payment_Source__c': 'Stripe', 'PMT_Invoice_ID__c': data['invoice_id'], 'PMT_Subscription_ID__c': data['subscription_id'], 'Proration_Amount__c': data['proration_amount'], 'RecordTypeId': settings.SUBHUB_OPP_RECORD_TYPE, 'Service_Plan__c': data['nickname_new'], 'Nickname_Old__c': data['nickname_old'], 'StageName': stage_name, })
def upsert_user(api_call_type, data): """ Update or insert (upsert) a contact record in SFDC @param int api_call_type: What kind of API call it was. Could be SUBSCRIBE, UNSUBSCRIBE, or SET. @param dict data: POST data from the form submission @return: """ key = data.get('email') or data.get('token') get_lock(key) upsert_contact(api_call_type, data, get_user_data(data.get('token'), data.get('email'), extra_fields=['id']))
def upsert_user(api_call_type, data): """ Update or insert (upsert) a contact record in SFDC @param int api_call_type: What kind of API call it was. Could be SUBSCRIBE, UNSUBSCRIBE, or SET. @param dict data: POST data from the form submission @return: """ key = data.get('email') or data.get('token') get_lock(key) upsert_contact(api_call_type, data, get_user_data(data.get('token'), data.get('email'), extra_fields=['id']))
def upsert_amo_user_data(data): """ Update AMO user data in the SFDC contact, or create a contact. Return the Contact data (the contact ID at a minimum). :param data: dict of amo user data :return: dict of SFDC contact data """ email = data.pop('email') amo_id = data.pop('id') amo_deleted = data.pop('deleted', False) amo_data = {f'amo_{k}': v for k, v in data.items() if v} amo_data['amo_user'] = not amo_deleted user = get_user_data(amo_id=amo_id, extra_fields=['id', 'amo_id']) if user: sfdc.update(user, amo_data) return user # include the ID in update or add since we couldn't find # the user with this ID above amo_data['amo_id'] = amo_id user = get_user_data(email=email, extra_fields=['id']) if user: sfdc.update(user, amo_data) # need amo_id for linking addons and authors user['amo_id'] = amo_id return user amo_data['email'] = email amo_data['source_url'] = 'https://addons.mozilla.org/' # returns only the new user ID in a dict, but that will work # when passed to e.g. `sfdc.update()` user = sfdc.add(amo_data) # need amo_id for linking addons and authors user['amo_id'] = amo_id return user
def send_recovery_message_task(email): user_data = get_user_data(email=email, extra_fields=['id']) if not user_data: log.debug("In send_recovery_message_task, email not known: %s" % email) return # make sure we have a language and format, no matter what ET returned lang = user_data.get('lang', 'en') or 'en' format = user_data.get('format', 'H') or 'H' if lang not in settings.RECOVER_MSG_LANGS: lang = 'en' message_id = mogrify_message_id(RECOVERY_MESSAGE_ID, lang, format) send_message.delay(message_id, email, user_data['id'], token=user_data['token'])
def send_recovery_message_task(email): user_data = get_user_data(email=email, extra_fields=['id']) if not user_data: log.debug("In send_recovery_message_task, email not known: %s" % email) return # make sure we have a language and format, no matter what ET returned lang = user_data.get('lang', 'en') or 'en' format = user_data.get('format', 'H') or 'H' if lang not in settings.RECOVER_MSG_LANGS: lang = 'en' message_id = mogrify_message_id(RECOVERY_MESSAGE_ID, lang, format) send_message.delay(message_id, email, user_data['id'], token=user_data['token'])
def send_recovery_message_task(email): user_data = get_user_data(email=email, extra_fields=["id"]) if not user_data: log.debug("In send_recovery_message_task, email not known: %s" % email) return # make sure we have a language and format, no matter what ET returned lang = user_data.get("lang", "en") or "en" format = user_data.get("format", "H") or "H" if lang not in settings.RECOVER_MSG_LANGS: lang = "en" message_id = mogrify_message_id(RECOVERY_MESSAGE_ID, lang, format) send_message.delay(message_id, email, user_data["id"], token=user_data["token"])
def process_subhub_event_payment_failed(data): """ Event name: invoice.payment_failed """ statsd.incr('news.tasks.process_subhub_event.payment_failed') user_data = get_user_data(payee_id=data['customer_id'], extra_fields=['id']) # the only user identifiable information available is the payment # processor/Stripe ID, so if the user wasn't found by that, there's really # nothing to be done here but retry. if not user_data: statsd.incr( 'news.tasks.process_subhub_event.payment_failed.user_not_found') raise RetryTask('Could not find user. Try again.') nickname = data['nickname'] if isinstance(nickname, list): nickname = nickname[0] sfdc.opportunity.create({ 'Amount': cents_to_dollars(data['amount_due']), 'CloseDate': iso_format_unix_timestamp(data['created']), 'Donation_Contact__c': user_data['id'], 'Event_Id__c': data['event_id'], 'Event_Name__c': data['event_type'], 'Name': 'Subscription Services', 'PMT_Subscription_ID__c': data['subscription_id'], 'PMT_Transaction_ID__c': data['charge_id'], 'Payment_Source__c': 'Stripe', 'RecordTypeId': settings.SUBHUB_OPP_RECORD_TYPE, 'Service_Plan__c': nickname, 'StageName': 'Payment Failed', 'currency__c': data['currency'], })
def process_subhub_event_subscription_cancel(data): """ Event name: customer.subscription_cancelled or customer.deleted """ statsd.incr('news.tasks.process_subhub_event.subscription_cancel') user_data = get_user_data(payee_id=data['customer_id'], extra_fields=['id']) if not user_data: statsd.incr( 'news.tasks.process_subhub_event_subscription_cancel.user_not_found' ) raise RetryTask('Could not find user. Try again.') nickname = data['nickname'] if isinstance(nickname, list): nickname = nickname[0] sfdc.opportunity.create({ 'Amount': cents_to_dollars(data['plan_amount']), 'Billing_Cycle_End__c': iso_format_unix_timestamp(data['current_period_end']), 'Billing_Cycle_Start__c': iso_format_unix_timestamp(data['current_period_start']), 'CloseDate': iso_format_unix_timestamp(data.get('cancel_at', time())), 'Donation_Contact__c': user_data['id'], 'Event_Id__c': data['event_id'], 'Event_Name__c': data['event_type'], 'Name': 'Subscription Services', 'Payment_Source__c': 'Stripe', 'PMT_Subscription_ID__c': data['subscription_id'], 'RecordTypeId': settings.SUBHUB_OPP_RECORD_TYPE, 'Service_Plan__c': nickname, 'StageName': SUB_STAGE_NAMES[data['event_type']], }) if data['event_type'] == 'customer.deleted': sfdc.update(user_data, {'fxa_deleted': True})
def record_common_voice_update(data): # do not change the sent data in place. A retry will use the changed data. dcopy = data.copy() email = dcopy.pop("email") user_data = get_user_data(email=email, extra_fields=["id"]) new_data = { "source_url": "https://voice.mozilla.org", "newsletters": [settings.COMMON_VOICE_NEWSLETTER], } for k, v in dcopy.items(): new_data["cv_" + k] = v if user_data: sfdc.update(user_data, new_data) else: new_data.update({"email": email, "token": generate_token()}) sfdc.add(new_data)
def record_common_voice_goals(data): email = data.pop('email') user_data = get_user_data(email=email, extra_fields=['id']) new_data = { 'source_url': 'https://voice.mozilla.org', 'newsletters': [settings.COMMON_VOICE_NEWSLETTER], } for k, v in data.items(): new_data['cv_' + k] = v if user_data: sfdc.update(user_data, new_data) else: new_data.update({ 'email': email, 'token': generate_token(), }) sfdc.add(new_data)
def process_subhub_event_subscription_reactivated(data): statsd.incr('news.tasks.process_subhub_event.subscription_reactivated') user_data = get_user_data(payee_id=data['customer_id'], extra_fields=['id']) if not user_data: statsd.incr( 'news.tasks.process_subhub_event.subscription_reactivated.user_not_found' ) raise RetryTask('Could not find user. Try again.') nickname = data['nickname'] if isinstance(nickname, list): nickname = nickname[0] sfdc.opportunity.create({ 'Amount': cents_to_dollars(data['plan_amount']), 'Billing_Cycle_End__c': iso_format_unix_timestamp(data['current_period_end']), 'CloseDate': iso_format_unix_timestamp(data.get('close_date', time())), 'Credit_Card_Type__c': data['brand'], 'Last_4_Digits__c': data['last4'], 'Donation_Contact__c': user_data['id'], 'Event_Id__c': data['event_id'], 'Event_Name__c': data['event_type'], 'Name': 'Subscription Services', 'Payment_Source__c': 'Stripe', 'PMT_Subscription_ID__c': data['subscription_id'], 'RecordTypeId': settings.SUBHUB_OPP_RECORD_TYPE, 'Service_Plan__c': nickname, 'StageName': 'Reactivation', })
def sfdc_add_update(update_data, user_data=None): # for use with maintenance mode only # TODO remove after maintenance is over and queue is processed if user_data: sfdc.update(user_data, update_data) else: try: sfdc.add(update_data) except sfapi.SalesforceMalformedRequest as e: # noqa # possibly a duplicate email. try the update below. user_data = get_user_data(email=update_data['email'], extra_fields=['id']) if user_data: # we have a user, delete generated token # and continue with an update update_data.pop('token', None) sfdc.update(user_data, update_data) else: # still no user, try the add one more time sfdc.add(update_data)
def sfdc_add_update(update_data, user_data=None): # for use with maintenance mode only # TODO remove after maintenance is over and queue is processed if user_data: sfdc.update(user_data, update_data) else: try: sfdc.add(update_data) except sfapi.SalesforceMalformedRequest as e: # noqa # possibly a duplicate email. try the update below. user_data = get_user_data(email=update_data['email'], extra_fields=['id']) if user_data: # we have a user, delete generated token # and continue with an update update_data.pop('token', None) sfdc.update(user_data, update_data) else: # still no user, try the add one more time sfdc.add(update_data)
def record_common_voice_goals(data): # do not change the sent data in place. A retry will use the changed data. dcopy = data.copy() email = dcopy.pop('email') user_data = get_user_data(email=email, extra_fields=['id']) new_data = { 'source_url': 'https://voice.mozilla.org', 'newsletters': [settings.COMMON_VOICE_NEWSLETTER], } for k, v in dcopy.items(): new_data['cv_' + k] = v if user_data: sfdc.update(user_data, new_data) else: new_data.update({ 'email': email, 'token': generate_token(), }) sfdc.add(new_data)
def confirm_user(token): """ Confirm any pending subscriptions for the user with this token. If any of the subscribed newsletters have welcome messages, send them. :param token: User's token :param user_data: Dictionary with user's data from Exact Target, as returned by get_user_data(), or None if that wasn't available when this was called. :raises: BasketError for fatal errors, NewsletterException for retryable errors. """ get_lock(token) user_data = get_user_data(token=token) if user_data is None: user = get_sfmc_doi_user(token) if user and user.get('email'): get_lock(user['email']) user['optin'] = True try: sfdc.add(user) except sfapi.SalesforceMalformedRequest: # probably already know the email address sfdc.update({'email': user['email']}, user) statsd.incr('news.tasks.confirm_user.moved_from_sfmc') else: statsd.incr('news.tasks.confirm_user.confirm_user_not_found') return if user_data['optin']: # already confirmed return if not ('email' in user_data and user_data['email']): raise BasketError('token has no email in ET') sfdc.update(user_data, {'optin': True})
def confirm_user(token): """ Confirm any pending subscriptions for the user with this token. If any of the subscribed newsletters have welcome messages, send them. :param token: User's token :param user_data: Dictionary with user's data from Exact Target, as returned by get_user_data(), or None if that wasn't available when this was called. :raises: BasketError for fatal errors, NewsletterException for retryable errors. """ get_lock(token) user_data = get_user_data(token=token) if user_data is None: user = get_sfmc_doi_user(token) if user and user.get('email'): get_lock(user['email']) user['optin'] = True try: sfdc.add(user) except sfapi.SalesforceMalformedRequest: # probably already know the email address sfdc.update({'email': user['email']}, user) statsd.incr('news.tasks.confirm_user.moved_from_sfmc') else: statsd.incr('news.tasks.confirm_user.confirm_user_not_found') return if user_data['optin']: # already confirmed return if not ('email' in user_data and user_data['email']): raise BasketError('token has no email in ET') sfdc.update(user_data, {'optin': True})
def process_subhub_event_subscription_charge(data): """ Event names: customer.subscription.created, customer.recurring_charge This method handles both new and recurring charges. Each of the handled events contains the same payload data. The only variation below is in regards to Initial_Purchase__c, which will be True for the `customer.subscription.created` event, and False for the `customer.recurring_charge` event. """ statsd.incr('news.tasks.process_subhub_event.subscription_charge') user_data = get_user_data(payee_id=data['customer_id'], extra_fields=['id']) if not user_data: statsd.incr( 'news.tasks.process_subhub_event.subscription_charge.user_not_found' ) raise RetryTask('Could not find user. Try again.') nickname = data['nickname'] if isinstance(nickname, list): nickname = nickname[0] # if a customer re-instates service after a cancellation, the record needs to be updated oppy_data = { 'Amount': cents_to_dollars(data['plan_amount']), 'Billing_Cycle_End__c': iso_format_unix_timestamp(data['current_period_end']), 'Billing_Cycle_Start__c': iso_format_unix_timestamp(data['current_period_start']), 'CloseDate': iso_format_unix_timestamp(data['created']), 'Credit_Card_Type__c': data['brand'], 'currency__c': data['currency'], 'Donation_Contact__c': user_data['id'], 'Event_Id__c': data['event_id'], 'Event_Name__c': data['event_type'], 'Initial_Purchase__c': data['event_type'] == 'customer.subscription.created', 'Invoice_Number__c': data['invoice_number'], 'Last_4_Digits__c': data['last4'], 'Name': 'Subscription Services', 'Next_Invoice_Date__c': iso_format_unix_timestamp(data['next_invoice_date']), 'Payment_Source__c': 'Stripe', 'PMT_Subscription_ID__c': data['subscription_id'], 'PMT_Transaction_ID__c': data['charge'], 'RecordTypeId': settings.SUBHUB_OPP_RECORD_TYPE, 'Service_Plan__c': nickname, 'StageName': 'Closed Won', } if 'proration_amount' in data: oppy_data['Proration_Amount__c'] = cents_to_dollars( data['proration_amount']) if 'total_amount' in data: oppy_data['Total_Amount__c'] = cents_to_dollars(data['total_amount']) sfdc.opportunity.upsert(f'PMT_Invoice_ID__c/{data["invoice_id"]}', oppy_data)
def process_donation(data): get_lock(data["email"]) contact_data = { "_set_subscriber": False, # SFDC, leave "subscriber" flag alone "mofo_relevant": True, # CTMS, set a MoFo relevant contact } # do "or ''" because data can contain None values first_name = (data.get("first_name") or "").strip() last_name = (data.get("last_name") or "").strip() if first_name and last_name: contact_data["first_name"] = first_name contact_data["last_name"] = last_name elif first_name: contact_data["first_name"] = first_name elif last_name: names = data["last_name"].rsplit(None, 1) if len(names) == 2: first, last = names else: first, last = "", names[0] if first: contact_data["first_name"] = first if last: contact_data["last_name"] = last user_data = get_user_data(email=data["email"], extra_fields=["id"]) if user_data: if contact_data and ( ( "first_name" in contact_data and contact_data["first_name"] != user_data["first_name"] ) or ( "last_name" in contact_data and contact_data["last_name"] != user_data["last_name"] ) ): sfdc.update(user_data, contact_data) ctms_data = contact_data.copy() del ctms_data["_set_subscriber"] ctms.update(user_data, ctms_data) else: contact_data["token"] = generate_token() contact_data["email"] = data["email"] contact_data["record_type"] = settings.DONATE_CONTACT_RECORD_TYPE ctms_data = contact_data.copy() del ctms_data["_set_subscriber"] del ctms_data["record_type"] contact = ctms.add(ctms_data) if contact: contact_data["email_id"] = contact["email"]["email_id"] if not settings.SFDC_ENABLED: return # returns a dict with the new ID but no other user data, but that's enough here user_data = sfdc.add(contact_data) if not user_data.get("id"): # retry here to make sure we associate the donation data with the proper account raise RetryTask("User not yet available") if not settings.SFDC_ENABLED: return # add opportunity donation = { "RecordTypeId": settings.DONATE_OPP_RECORD_TYPE, "Name": "Foundation Donation", "Donation_Contact__c": user_data["id"], "StageName": "Closed Won", "Amount": float(data["donation_amount"]), "Currency__c": data["currency"].upper(), "Payment_Source__c": data["service"], "PMT_Transaction_ID__c": data["transaction_id"], "Payment_Type__c": "Recurring" if data["recurring"] else "One-Time", } # https://github.com/mozmeao/basket/issues/364 if "campaign_id" in data: donation["CampaignId"] = data["campaign_id"] # this is a unix timestamp in ms since epoc timestamp = data.get("created") if timestamp: donation["CloseDate"] = iso_format_unix_timestamp(timestamp) for dest_name, source_name in DONATION_NEW_FIELDS.items(): if source_name in data: donation[dest_name] = data[source_name] for dest_name, source_name in DONATION_OPTIONAL_FIELDS.items(): if data.get(source_name): # truncate at 2000 chars as that's the max for # a SFDC text field. We may do more granular # truncation per field in future. donation[dest_name] = data[source_name][:2000] try: sfdc.opportunity.create(donation) except sfapi.SalesforceMalformedRequest as e: if e.content and e.content[0].get("errorCode") == "DUPLICATE_VALUE": # already in the system, ignore pass else: raise
def fxa_delete(data): sfmc.upsert_row('FXA_Deleted', {'FXA_ID': data['uid']}) user_data = get_user_data(fxa_id=data['uid'], extra_fields=['id']) if user_data: sfdc.update(user_data, {'fxa_deleted': True})
def update_user_task(request, api_call_type, data=None, optin=False, sync=False): """Call the update_user task async with the right parameters. If sync==True, be sure to include the token in the response. Otherwise, basket can just do everything in the background. """ data = data or request.POST.dict() newsletters = parse_newsletters_csv(data.get('newsletters')) if newsletters: if api_call_type == SUBSCRIBE: all_newsletters = newsletter_and_group_slugs() + get_transactional_message_ids() else: all_newsletters = newsletter_slugs() private_newsletters = newsletter_private_slugs() for nl in newsletters: if nl not in all_newsletters: return HttpResponseJSON({ 'status': 'error', 'desc': 'invalid newsletter', 'code': errors.BASKET_INVALID_NEWSLETTER, }, 400) if api_call_type != UNSUBSCRIBE and nl in private_newsletters: if not is_authorized(request, data.get('email')): return HttpResponseJSON({ 'status': 'error', 'desc': 'private newsletter subscription requires a valid API key or OAuth', 'code': errors.BASKET_AUTH_ERROR, }, 401) if 'lang' in data: if not language_code_is_valid(data['lang']): data['lang'] = 'en' elif 'accept_lang' in data: lang = get_best_language(get_accept_languages(data['accept_lang'])) if lang: data['lang'] = lang del data['accept_lang'] # if lang not provided get the best one from the accept-language header else: lang = get_best_request_lang(request) if lang: data['lang'] = lang email = data.get('email') token = data.get('token') if not (email or token): return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) if optin: data['optin'] = True if api_call_type == SUBSCRIBE and email and data.get('newsletters'): # only rate limit here so we don't rate limit errors. if is_ratelimited(request, group='basket.news.views.update_user_task.subscribe', key=lambda x, y: '%s-%s' % (data['newsletters'], email), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): raise Ratelimited() if api_call_type == SET and token and data.get('newsletters'): # only rate limit here so we don't rate limit errors. if is_ratelimited(request, group='basket.news.views.update_user_task.set', key=lambda x, y: '%s-%s' % (data['newsletters'], token), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): raise Ratelimited() if sync: statsd.incr('news.views.subscribe.sync') if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY: # save what we can upsert_user.delay(api_call_type, data, start_time=time()) # have to error since we can't return a token return HttpResponseJSON({ 'status': 'error', 'desc': 'sync is not available in maintenance mode', 'code': errors.BASKET_NETWORK_FAILURE, }, 400) try: user_data = get_user_data(email=email, token=token) except NewsletterException as e: return newsletter_exception_response(e) if not user_data: if not email: # must have email to create a user return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) token, created = upsert_contact(api_call_type, data, user_data) return HttpResponseJSON({ 'status': 'ok', 'token': token, 'created': created, }) else: upsert_user.delay(api_call_type, data, start_time=time()) return HttpResponseJSON({ 'status': 'ok', })
def update_user_task(request, api_call_type, data=None, optin=False, sync=False): """Call the update_user task async with the right parameters. If sync==True, be sure to include the token in the response. Otherwise, basket can just do everything in the background. """ data = data or request.POST.dict() newsletters = parse_newsletters_csv(data.get("newsletters")) if newsletters: if api_call_type == SUBSCRIBE: all_newsletters = (newsletter_and_group_slugs() + get_transactional_message_ids()) else: all_newsletters = newsletter_slugs() private_newsletters = newsletter_private_slugs() for nl in newsletters: if nl not in all_newsletters: return HttpResponseJSON( { "status": "error", "desc": "invalid newsletter", "code": errors.BASKET_INVALID_NEWSLETTER, }, 400, ) if api_call_type != UNSUBSCRIBE and nl in private_newsletters: if not is_authorized(request, data.get("email")): return HttpResponseJSON( { "status": "error", "desc": "private newsletter subscription requires a valid API key or OAuth", "code": errors.BASKET_AUTH_ERROR, }, 401, ) if "lang" in data: if not language_code_is_valid(data["lang"]): data["lang"] = "en" elif "accept_lang" in data: lang = get_best_language(get_accept_languages(data["accept_lang"])) if lang: data["lang"] = lang del data["accept_lang"] # if lang not provided get the best one from the accept-language header else: lang = get_best_request_lang(request) if lang: data["lang"] = lang # now ensure that if we do have a lang that it's a supported one if "lang" in data: data["lang"] = get_best_supported_lang(data["lang"]) email = data.get("email") token = data.get("token") if not (email or token): return HttpResponseJSON( { "status": "error", "desc": MSG_EMAIL_OR_TOKEN_REQUIRED, "code": errors.BASKET_USAGE_ERROR, }, 400, ) if optin: data["optin"] = True if api_call_type == SUBSCRIBE and email and data.get("newsletters"): # only rate limit here so we don't rate limit errors. if is_ratelimited( request, group="basket.news.views.update_user_task.subscribe", key=lambda x, y: "%s-%s" % (data["newsletters"], email), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True, ): raise Ratelimited() if api_call_type == SET and token and data.get("newsletters"): # only rate limit here so we don't rate limit errors. if is_ratelimited( request, group="basket.news.views.update_user_task.set", key=lambda x, y: "%s-%s" % (data["newsletters"], token), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True, ): raise Ratelimited() if sync: statsd.incr("news.views.subscribe.sync") if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY: # save what we can upsert_user.delay(api_call_type, data, start_time=time()) # have to error since we can't return a token return HttpResponseJSON( { "status": "error", "desc": "sync is not available in maintenance mode", "code": errors.BASKET_NETWORK_FAILURE, }, 400, ) try: user_data = get_user_data(email=email, token=token) except NewsletterException as e: return newsletter_exception_response(e) if not user_data: if not email: # must have email to create a user return HttpResponseJSON( { "status": "error", "desc": MSG_EMAIL_OR_TOKEN_REQUIRED, "code": errors.BASKET_USAGE_ERROR, }, 400, ) token, created = upsert_contact(api_call_type, data, user_data) return HttpResponseJSON({ "status": "ok", "token": token, "created": created }) else: upsert_user.delay(api_call_type, data, start_time=time()) return HttpResponseJSON({"status": "ok"})
def update_user_task(request, api_call_type, data=None, optin=False, sync=False): """Call the update_user task async with the right parameters. If sync==True, be sure to include the token in the response. Otherwise, basket can just do everything in the background. """ data = data or request.POST.dict() newsletters = parse_newsletters_csv(data.get('newsletters')) if newsletters: if api_call_type == SUBSCRIBE: all_newsletters = newsletter_and_group_slugs() + get_transactional_message_ids() else: all_newsletters = newsletter_slugs() private_newsletters = newsletter_private_slugs() for nl in newsletters: if nl not in all_newsletters: return HttpResponseJSON({ 'status': 'error', 'desc': 'invalid newsletter', 'code': errors.BASKET_INVALID_NEWSLETTER, }, 400) if api_call_type != UNSUBSCRIBE and nl in private_newsletters: if not has_valid_api_key(request): return HttpResponseJSON({ 'status': 'error', 'desc': 'private newsletter subscription requires a valid API key', 'code': errors.BASKET_AUTH_ERROR, }, 401) if 'lang' in data: if not language_code_is_valid(data['lang']): data['lang'] = 'en' elif 'accept_lang' in data: lang = get_best_language(get_accept_languages(data['accept_lang'])) if lang: data['lang'] = lang del data['accept_lang'] else: data['lang'] = 'en' # if lang not provided get the best one from the accept-language header else: data['lang'] = get_best_request_lang(request) or 'en' email = data.get('email') token = data.get('token') if not (email or token): return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) if optin: data['optin'] = True if api_call_type == SUBSCRIBE and email and data.get('newsletters'): # only rate limit here so we don't rate limit errors. if is_ratelimited(request, group='basket.news.views.update_user_task.subscribe', key=lambda x, y: '%s-%s' % (data['newsletters'], email), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): raise Ratelimited() if api_call_type == SET and token and data.get('newsletters'): # only rate limit here so we don't rate limit errors. if is_ratelimited(request, group='basket.news.views.update_user_task.set', key=lambda x, y: '%s-%s' % (data['newsletters'], token), rate=EMAIL_SUBSCRIBE_RATE_LIMIT, increment=True): raise Ratelimited() if sync: statsd.incr('news.views.subscribe.sync') if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY: # save what we can upsert_user.delay(api_call_type, data, start_time=time()) # have to error since we can't return a token return HttpResponseJSON({ 'status': 'error', 'desc': 'sync is not available in maintenance mode', 'code': errors.BASKET_NETWORK_FAILURE, }, 400) try: user_data = get_user_data(email=email, token=token) except NewsletterException as e: return newsletter_exception_response(e) if not user_data: if not email: # must have email to create a user return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) token, created = upsert_contact(api_call_type, data, user_data) return HttpResponseJSON({ 'status': 'ok', 'token': token, 'created': created, }) else: upsert_user.delay(api_call_type, data, start_time=time()) return HttpResponseJSON({ 'status': 'ok', })
def lookup_user(request): """Lookup a user in Exact Target given email or token (not both). To look up by email, a valid API key are required. If email and token are both provided, an error is returned rather than trying to define all the possible behaviors. SSL is always required when using this call. If no SSL, it'll fail with 401 and an appropriate message in the response body. Response content is always JSON. If user is not found, returns a 404 status and json is:: { 'status': 'error', 'desc': 'No such user' } (If you need to distinguish user not found from an error calling the API, check the response content.) If a required, valid API key is not provided, status is 401 Unauthorized. The API key can be provided either as a GET query parameter ``api-key`` or a request header ``X-api-key``. If it's provided as a query parameter, any request header is ignored. For other errors, similarly response status is 4xx and the json 'desc' says what's wrong. Otherwise, status is 200 and json is the return value from `get_user_data`. See that method for details. Note that because this method always calls Exact Target one or more times, it can be slower than some other Basket APIs, and will fail if ET is down. """ if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY: # can't return user data during maintenance return HttpResponseJSON({ 'status': 'error', 'desc': 'user data is not available in maintenance mode', 'code': errors.BASKET_NETWORK_FAILURE, }, 400) token = request.GET.get('token', None) email = request.GET.get('email', None) if (not email and not token) or (email and token): return HttpResponseJSON({ 'status': 'error', 'desc': MSG_EMAIL_OR_TOKEN_REQUIRED, 'code': errors.BASKET_USAGE_ERROR, }, 400) if email and not has_valid_api_key(request): return HttpResponseJSON({ 'status': 'error', 'desc': 'Using lookup_user with `email`, you need to pass a ' 'valid `api-key` GET parameter or X-api-key header', 'code': errors.BASKET_AUTH_ERROR, }, 401) if email: email = process_email(email) if not email: return invalid_email_response() try: user_data = get_user_data(token=token, email=email) except NewsletterException as e: return newsletter_exception_response(e) status_code = 200 if not user_data: code = errors.BASKET_UNKNOWN_TOKEN if token else errors.BASKET_UNKNOWN_EMAIL user_data = { 'status': 'error', 'desc': MSG_USER_NOT_FOUND, 'code': code, } status_code = 404 return HttpResponseJSON(user_data, status_code)
def fxa_callback(request): # remove state from session to prevent multiple attempts error_url = f"https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/fxa-error/" sess_state = request.session.pop("fxa_state", None) if sess_state is None: statsd.incr("news.views.fxa_callback.error.no_state") return HttpResponseRedirect(error_url) code = request.GET.get("code") state = request.GET.get("state") if not (code and state): statsd.incr("news.views.fxa_callback.error.no_code_state") return HttpResponseRedirect(error_url) if sess_state != state: statsd.incr("news.views.fxa_callback.error.no_state_match") return HttpResponseRedirect(error_url) fxa_oauth, fxa_profile = get_fxa_clients() try: access_token = fxa_oauth.trade_code( code, ttl=settings.FXA_OAUTH_TOKEN_TTL)["access_token"] user_profile = fxa_profile.get_profile(access_token) except Exception: statsd.incr("news.views.fxa_callback.error.fxa_comm") sentry_sdk.capture_exception() return HttpResponseRedirect(error_url) email = user_profile["email"] try: user_data = get_user_data(email=email) except SalesforceError: statsd.incr("news.views.fxa_callback.error.get_user_data") sentry_sdk.capture_exception() return HttpResponseRedirect(error_url) if user_data: token = user_data["token"] else: new_user_data = { "email": email, "optin": True, "format": "H", "newsletters": [settings.FXA_REGISTER_NEWSLETTER], "source_url": f"{settings.FXA_REGISTER_SOURCE_URL}?utm_source=basket-fxa-oauth", } locale = user_profile.get("locale") if locale: new_user_data["fxa_lang"] = locale lang = get_best_language(get_accept_languages(locale)) if lang not in newsletter_languages(): lang = "other" new_user_data["lang"] = lang try: token = upsert_contact(SUBSCRIBE, new_user_data, None)[0] except SalesforceError: statsd.incr("news.views.fxa_callback.error.upsert_contact") sentry_sdk.capture_exception() return HttpResponseRedirect(error_url) redirect_to = ( f"https://{settings.FXA_EMAIL_PREFS_DOMAIN}/newsletter/existing/{token}/?fxa=1" ) return HttpResponseRedirect(redirect_to)
def process_donation(data): get_lock(data['email']) # tells the backend to leave the "subscriber" flag alone contact_data = {'_set_subscriber': False} # do "or ''" because data can contain None values first_name = (data.get('first_name') or '').strip() last_name = (data.get('last_name') or '').strip() if first_name and last_name: contact_data['first_name'] = first_name contact_data['last_name'] = last_name elif first_name: contact_data['first_name'] = first_name elif last_name: names = data['last_name'].rsplit(None, 1) if len(names) == 2: first, last = names else: first, last = '', names[0] if first: contact_data['first_name'] = first if last: contact_data['last_name'] = last user_data = get_user_data(email=data['email'], extra_fields=['id']) if user_data: if contact_data and ( ('first_name' in contact_data and contact_data['first_name'] != user_data['first_name']) or ('last_name' in contact_data and contact_data['last_name'] != user_data['last_name'])): sfdc.update(user_data, contact_data) else: contact_data['token'] = generate_token() contact_data['email'] = data['email'] contact_data['record_type'] = settings.DONATE_CONTACT_RECORD_TYPE # returns a dict with the new ID but no other user data, but that's enough here user_data = sfdc.add(contact_data) if not user_data.get('id'): # retry here to make sure we associate the donation data with the proper account raise RetryTask('User not yet available') # add opportunity donation = { 'RecordTypeId': settings.DONATE_OPP_RECORD_TYPE, 'Name': 'Foundation Donation', 'Donation_Contact__c': user_data['id'], 'StageName': 'Closed Won', 'Amount': float(data['donation_amount']), 'Currency__c': data['currency'].upper(), 'Payment_Source__c': data['service'], 'PMT_Transaction_ID__c': data['transaction_id'], 'Payment_Type__c': 'Recurring' if data['recurring'] else 'One-Time', } # this is a unix timestamp in ms since epoc timestamp = data.get('created') if timestamp: donation['CloseDate'] = iso_format_unix_timestamp(timestamp) for dest_name, source_name in DONATION_NEW_FIELDS.items(): if source_name in data: donation[dest_name] = data[source_name] for dest_name, source_name in DONATION_OPTIONAL_FIELDS.items(): if data.get(source_name): # truncate at 2000 chars as that's the max for # a SFDC text field. We may do more granular # truncation per field in future. donation[dest_name] = data[source_name][:2000] try: sfdc.opportunity.create(donation) except sfapi.SalesforceMalformedRequest as e: if e.content and e.content[0].get('errorCode') == 'DUPLICATE_VALUE': # already in the system, ignore pass else: raise
def lookup_user(request): """Lookup a user in Exact Target given email or token (not both). To look up by email, a valid API key are required. If email and token are both provided, an error is returned rather than trying to define all the possible behaviors. SSL is always required when using this call. If no SSL, it'll fail with 401 and an appropriate message in the response body. Response content is always JSON. If user is not found, returns a 404 status and json is:: { 'status': 'error', 'desc': 'No such user' } (If you need to distinguish user not found from an error calling the API, check the response content.) If a required, valid API key is not provided, status is 401 Unauthorized. The API key can be provided either as a GET query parameter ``api-key`` or a request header ``X-api-key``. If it's provided as a query parameter, any request header is ignored. For other errors, similarly response status is 4xx and the json 'desc' says what's wrong. Otherwise, status is 200 and json is the return value from `get_user_data`. See that method for details. Note that because this method always calls Exact Target one or more times, it can be slower than some other Basket APIs, and will fail if ET is down. """ if settings.MAINTENANCE_MODE and not settings.MAINTENANCE_READ_ONLY: # can't return user data during maintenance return HttpResponseJSON( { "status": "error", "desc": "user data is not available in maintenance mode", "code": errors.BASKET_NETWORK_FAILURE, }, 400, ) 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( { "status": "error", "desc": MSG_EMAIL_OR_TOKEN_REQUIRED, "code": errors.BASKET_USAGE_ERROR, }, 400, ) if email and not is_authorized(request, email): return HttpResponseJSON( { "status": "error", "desc": "Using lookup_user with `email`, you need to pass a " "valid `api-key` or FxA OAuth Autorization header.", "code": errors.BASKET_AUTH_ERROR, }, 401, ) if email: email = process_email(email) if not email: return invalid_email_response() try: user_data = get_user_data(token=token, email=email, get_fxa=get_fxa) except NewsletterException as e: return newsletter_exception_response(e) status_code = 200 if not user_data: code = errors.BASKET_UNKNOWN_TOKEN if token else errors.BASKET_UNKNOWN_EMAIL user_data = { "status": "error", "desc": MSG_USER_NOT_FOUND, "code": code, } status_code = 404 return HttpResponseJSON(user_data, status_code)
def process_donation(data): get_lock(data['email']) # tells the backend to leave the "subscriber" flag alone contact_data = {'_set_subscriber': False} # do "or ''" because data can contain None values first_name = (data.get('first_name') or '').strip() last_name = (data.get('last_name') or '').strip() if first_name and last_name: contact_data['first_name'] = first_name contact_data['last_name'] = last_name elif first_name: contact_data['first_name'] = first_name elif last_name: names = data['last_name'].rsplit(None, 1) if len(names) == 2: first, last = names else: first, last = '', names[0] if first: contact_data['first_name'] = first if last: contact_data['last_name'] = last user_data = get_user_data(email=data['email'], extra_fields=['id']) if user_data: if contact_data and ( ('first_name' in contact_data and contact_data['first_name'] != user_data['first_name']) or ('last_name' in contact_data and contact_data['last_name'] != user_data['last_name'])): sfdc.update(user_data, contact_data) else: contact_data['token'] = generate_token() contact_data['email'] = data['email'] contact_data['record_type'] = settings.DONATE_CONTACT_RECORD_TYPE sfdc.add(contact_data) # fetch again to get ID user_data = get_user_data(email=data.get('email'), extra_fields=['id']) if not user_data: # retry here to make sure we associate the donation data with the proper account raise RetryTask('User not yet available') # add opportunity donation = { 'RecordTypeId': settings.DONATE_OPP_RECORD_TYPE, 'Name': 'Foundation Donation', 'Donation_Contact__c': user_data['id'], 'StageName': 'Closed Won', 'Amount': float(data['donation_amount']), 'Currency__c': data['currency'].upper(), 'Payment_Source__c': data['service'], 'PMT_Transaction_ID__c': data['transaction_id'], 'Payment_Type__c': 'Recurring' if data['recurring'] else 'One-Time', } # this is a unix timestamp in ms since epoc timestamp = data.get('created') if timestamp: donation['CloseDate'] = iso_format_unix_timestamp(timestamp) for dest_name, source_name in DONATION_NEW_FIELDS.items(): if source_name in data: donation[dest_name] = data[source_name] for dest_name, source_name in DONATION_OPTIONAL_FIELDS.items(): if source_name in data: # truncate at 2000 chars as that's the max for # a SFDC text field. We may do more granular # truncation per field in future. donation[dest_name] = data[source_name][:2000] try: sfdc.opportunity.create(donation) except sfapi.SalesforceMalformedRequest as e: if e.content and e.content[0].get('errorCode') == 'DUPLICATE_VALUE': # already in the system, ignore pass else: raise