예제 #1
0
파일: tasks.py 프로젝트: shtalinberg/basket
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
예제 #2
0
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
예제 #3
0
파일: tasks.py 프로젝트: mozmar/basket
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
예제 #4
0
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)
예제 #5
0
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})
예제 #6
0
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)
예제 #7
0
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"})
예제 #8
0
파일: views.py 프로젝트: pmac/basket
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'})
예제 #9
0
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
예제 #10
0
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
예제 #11
0
파일: tasks.py 프로젝트: shtalinberg/basket
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
예제 #12
0
파일: tasks.py 프로젝트: shtalinberg/basket
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
예제 #13
0
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,
    })
예제 #14
0
파일: tasks.py 프로젝트: mozmar/basket
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']))
예제 #15
0
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']))
예제 #16
0
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
예제 #17
0
파일: tasks.py 프로젝트: mozmar/basket
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'])
예제 #18
0
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'])
예제 #19
0
파일: tasks.py 프로젝트: shtalinberg/basket
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"])
예제 #20
0
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'],
    })
예제 #21
0
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})
예제 #22
0
파일: tasks.py 프로젝트: shtalinberg/basket
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)
예제 #23
0
파일: tasks.py 프로젝트: mozmar/basket
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)
예제 #24
0
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',
    })
예제 #25
0
파일: tasks.py 프로젝트: mozmar/basket
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)
예제 #26
0
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)
예제 #27
0
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)
예제 #28
0
파일: tasks.py 프로젝트: mozmar/basket
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})
예제 #29
0
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})
예제 #30
0
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)
예제 #31
0
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
예제 #32
0
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})
예제 #33
0
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',
        })
예제 #34
0
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"})
예제 #35
0
파일: views.py 프로젝트: pmac/basket
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',
        })
예제 #36
0
파일: views.py 프로젝트: pmac/basket
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)
예제 #37
0
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)
예제 #38
0
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
예제 #39
0
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)
예제 #40
0
파일: tasks.py 프로젝트: mozmar/basket
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