예제 #1
0
def purchase_api():
    '''process the given tx_hash and return the payed-for goods'''

    #TODO: at some point we should try to listen in on incoming tx_hashes
    # for our account(s). this should hasten the process of reedeming offers.
    payload = request.get_json(silent=True)
    try:
        user_id = extract_header(request)
        tx_hash = payload.get('tx_hash', None)
        if None in (user_id, tx_hash):
            raise InvalidUsage('invalid param')
    except Exception as e:
        print('exception: %s' % e)
        raise InvalidUsage('bad-request')

    try:
        # process the tx_hash, provided its not already being processed by another server
        lock = redis_lock.Lock(app.redis, 'redeem:%s' % tx_hash)
        if lock.acquire(blocking=False):
            success, goods = process_order(user_id, tx_hash)
            if not success:
                raise InvalidUsage('cant redeem with tx_hash:%s' % tx_hash)
            increment_metric('offers_redeemed')
            return jsonify(status='ok', goods=goods)
        else:
            return jsonify(status='error', reason='already processing tx_hash')
    finally:
        lock.release()
예제 #2
0
def send_p2p_push(user_id, amount, tx_dict):
    """sends a push to the given userid to inform of p2p tx"""
    push_id = generate_push_id()
    push_type = 'engage-recent'
    from kinappserver.models import get_user_push_data
    os_type, token, push_env = get_user_push_data(user_id)
    if token:
        if os_type == OS_ANDROID:
            increment_metric('p2p-tx-push-gcm')
            print('sending p2p-tx push message to GCM user %s' % user_id)
            push_send_gcm(
                token,
                gcm_payload(push_type, push_id, {
                    'title': '',
                    'body': "A friend just sent you %sKIN!" % amount
                }), push_env)
        else:
            increment_metric('p2p-tx-push-ios')
            print('sending p2p-tx push message to APNS user %s' % user_id)
            push_send_apns(
                token,
                apns_payload("", "A friend just sent you %sKIN!" % amount,
                             'p2p_received', push_id, 'default',
                             {'tx': tx_dict}), push_env)
    else:
        print('not sending p2p-tx push to user_id %s: no token' % user_id)
    return
예제 #3
0
def quest_answers():
    '''receive the results for a tasks and pay the user for them'''
    payload = request.get_json(silent=True)
    try:
        user_id = extract_header(request)
        task_id = payload.get('id', None)
        address = payload.get('address', None)
        results = payload.get('results', None)
        send_push = payload.get('send_push', True)
        if None in (user_id, task_id, address, results):
            raise InvalidUsage('bad-request')
        #TODO more input checks here
    except Exception as e:
        raise InvalidUsage('bad-request')

    if not store_task_results(user_id, task_id, results):
        # should never happen: the client sent the results too soon
        print('rejecting user %s task %s results' % (user_id, task_id))
        increment_metric('premature_task_results')
        return jsonify(status='error',
                       reason='cooldown_enforced'), status.HTTP_400_BAD_REQUEST
    try:
        memo = utils.KINIT_MEMO_PREFIX + str(uuid4(
        ))[:utils.
           ORDER_ID_LENGTH]  # generate a memo string and send it to the client
        reward_store_and_push(address, task_id, send_push, user_id, memo)
    except Exception as e:
        print('exception: %s' % e)
        print('failed to reward task %s at address %s' % (task_id, address))

    increment_metric('task_completed')
    return jsonify(status='ok', memo=str(memo))
예제 #4
0
def send_please_upgrade_push_2_inner(user_id):
    """sends a push to the given userid to please upgrade"""
    push_id = generate_push_id()
    push_type = 'please_upgrade'
    from kinappserver.models import get_user_push_data
    os_type, token, push_env = get_user_push_data(user_id)
    if token:
        if os_type == OS_ANDROID:
            increment_metric('pleaseupgrade-android')
            print('sending please-upgrade push message to GCM user %s' %
                  user_id)
            push_send_gcm(
                token,
                gcm_payload(
                    'engage-recent', push_id, {
                        'title':
                        '',
                        'body':
                        "Your current version of Kinit is no longer supported. Please download the newest version from Google Play"
                    }), push_env)
        else:
            increment_metric('pleaseupgrade-ios')
            print('sending please-upgrade push message to APNS user %s' %
                  user_id)
            push_send_apns(
                token,
                apns_payload(
                    "",
                    "Your current version of Kinit is no longer supported. Please download the newest version from the App Store",
                    push_type, push_id), push_env)
    else:
        print('not sending please-upgrade push to user_id %s: no token' %
              user_id)
    return
예제 #5
0
def send_kin_with_payment_service(public_address, amount, memo=None):
    """send kins to an address using the payment service"""

    #  sanity:
    if public_address in (None, ''):
        log.error('cant send kin to address: %s' % public_address)
        return False, None

    if amount is None or amount < 1:
        log.error('cant send kin amount: %s' % amount)
        return False, None

    print('sending kin to address: %s' % public_address)
    headers = {
        'X-REQUEST-ID': str(random.randint(1, 1000000))
    }  # doesn't actually matter
    payment_payload = {
        'id': memo,
        'amount': amount,
        'app_id': 'kit',
        'recipient_address': public_address,
        'callback': "%s/payments/callback" % config.API_SERVER_URL
    }

    try:
        res = requests.post('%s/payments' % config.PAYMENT_SERVICE_URL,
                            headers=headers,
                            json=payment_payload)
        res.raise_for_status()
    except Exception as e:
        increment_metric('send_kin_error')
        print(
            'caught exception sending %s kin to address %s using the payment service'
            % (amount, public_address))
        print(e)
예제 #6
0
def register_api():
    ''' register a user to the system
    called once by every client until 200OK is received from the server.
    the payload may contain a optional push token.
    '''
    payload = request.get_json(silent=True)
    try:
        # add redis lock here?
        user_id = payload.get('user_id', None)
        os = payload.get('os', None)
        device_model = payload.get('device_model', None)
        token = payload.get('token', None)
        time_zone = payload.get('time_zone', None)
        device_id = payload.get('device_id', None)
        app_ver = payload.get('app_ver', None)
        #TODO more input check on the values
        if None in (
                user_id, os, device_model, time_zone, app_ver
        ):  # token is optional, device-id is required but may be None
            raise InvalidUsage('bad-request')
        if os not in ('iOS', 'android'):
            raise InvalidUsage('bad-request')
        user_id = UUID(user_id)  # throws exception on invalid uuid
    except Exception as e:
        raise InvalidUsage('bad-request')
    else:
        try:
            create_user(user_id, os, device_model, token, time_zone, device_id,
                        app_ver)
        except InvalidUsage as e:
            raise InvalidUsage('duplicate-userid')
        else:
            print('created user with user_id %s' % (user_id))
            increment_metric('user_registered')
            return jsonify(status='ok')
예제 #7
0
def release_unclaimed_api():
    '''endpoint used to get the current balance'''
    if not config.DEBUG:
        limit_to_local_host()
    released = release_unclaimed_goods()
    increment_metric('unclaimed_released', released)
    return jsonify(status='ok', released=released)
예제 #8
0
def release_unclaimed_api():
    """endpoint used to release goods that were booked but never redeemed"""
    if not config.DEBUG:
        limit_to_localhost()

    released = release_unclaimed_goods()
    increment_metric('unclaimed_released', released)
    return jsonify(status='ok', released=released)
예제 #9
0
def process_order(user_id, tx_hash):
    '''release the goods to the user, provided that they've been payed for'''
    # extract the tx_data from the blockchain
    goods = []
    res, tx_data = stellar.extract_tx_payment_data(tx_hash)
    if not res:
        print('could not extract tx_data for tx_hash: %s' % tx_hash)
        return False, None

    # get the order from the db using the memo in the tx
    order = get_order_by_order_id(tx_data['memo'])
    if not order:
        print('cant match tx order_id to any active orders')
        return False, None

    # ensure the tx matches the order
    if tx_data['to_address'] != order.address:
        print('tx address does not match offer address')
        return False, None
    if int(tx_data['amount']) != order.kin_amount:
        print('tx amount does not match offer amount')
        return False, None

    # tx matched! docuemnt the tx in the db with a tx object
    create_tx(tx_hash, user_id, order.address, True, order.kin_amount,
              {'offer_id': str(order.offer_id)})

    # prevent doomsday scenario: ensure that the user's expected balance matches his actual balance
    expected_balance = expected_user_kin_balance(user_id)
    actual_balance = get_current_user_kin_balance(user_id)
    if actual_balance is None:
        print('doomsday: cant get user balance, so skipping check')
        increment_metric('doomsday-skipped')
    elif abs(expected_balance - actual_balance) > 100:
        print(
            'doomsday detected and rejected: userid: %s, expected %s, actual: %s'
            % (user_id, expected_balance, actual_balance))
        increment_metric('doomsday-rejected')
        return False, None
    else:
        print(
            'doomsday checked and passed: userid: %s, expected %s, actual: %s'
            % (user_id, expected_balance, actual_balance))

    # get the allocated goods
    res, good = finalize_good(order.order_id, tx_hash)
    if res:
        goods.append(good)
    else:
        print('failed to finalize the good for tx_hash: %s' % tx_hash)

    # delete the order
    try:
        delete_order(order.order_id)
    except Exception as e:
        print('failed to delete order %s' % order.order_id)

    return True, goods
예제 #10
0
def extract_tx_payment_data(tx_hash):
    """ensures that the given tx_hash is a valid payment tx,
       and return a dict with the memo, amount and to_address"""
    if tx_hash is None:
        raise InvalidUsage('invlid params')

    # get the tx_hash data. this might take a second,
    # so retry while 'Resource Missing' is recevied
    count = 0
    tx_data = None
    while (count < config.STELLAR_TIMEOUT_SEC):
        try:
            tx_data = app.kin_sdk.get_transaction_data(tx_hash)

        except kin.ResourceNotFoundError as e:
            count = count + 1
            sleep(1)
        else:
            break

    if tx_data is None:
        print('could not get tx_data for tx_hash: %s. waited %s seconds' %
              (tx_hash, count))
        increment_metric('tx_data_timeout')
        return False, {}

    if len(tx_data.operations) != 1:
        print('too many ops')
        return False, {}

    # get the first (and only) op:
    op = tx_data.operations[0]

    # verify op type
    if op['type'] != 'payment':
        print('unexpected type: %s' % op['type'])
        return False, {}

    # verify asset params
    if op['asset_code'] != ASSET_NAME and op['asset_issuer'] != \
            config.STELLAR_KIN_ISSUER_ADDRESS and op['asset_type'] != 'credit_alphanum4':
        print('unexpected asset-code/issuer/asset_type')
        return False, {}

    # verify memo type
    if tx_data['memo_type'] != 'text':
        print('unexpected memo type')
        return False, {}

    # assemble the result dict
    data = {}
    data['memo'] = tx_data.get('memo', None)
    data['amount'] = op.get('amount', None)
    data['to_address'] = op.get('to_address', None)
    return True, data
예제 #11
0
def create_account(public_address, initial_xlm_amount):
    """create an account for the given public address"""
    #TODO all repeating logic?
    print('creating account with balance:%s' % initial_xlm_amount)
    try:
        return app.kin_sdk.create_account(public_address,
                                          starting_balance=initial_xlm_amount)
    except Exception as e:
        increment_metric('create_account_error')
        print('caught exception creating account for address %s' %
              (public_address))
        print(e)
예제 #12
0
def compensate_user_api():
    """internal endpoint used to manually compensate users for missing txs"""
    if not config.DEBUG:
        limit_to_localhost()

    payload = request.get_json(silent=True)
    user_id = payload.get('user_id', None)
    kin_amount = int(payload.get('kin_amount', None))
    task_id = payload.get('task_id', None)
    memo = utils.generate_memo(is_manual=True)
    if None in (user_id, kin_amount, task_id):
        raise InvalidUsage('invalid param')
    public_address = get_address_by_userid(user_id)
    if not public_address:
        log.error('cant compensate user %s - no public address' % user_id)
        return jsonify(status='error', reason='no_public_address')

    user_tx_task_ids = [
        tx.tx_info.get('task_id', '-1')
        for tx in list_user_transactions(user_id)
    ]
    if task_id in user_tx_task_ids:
        print(
            'refusing to compensate user %s for task %s - already received funds!'
            % (user_id, task_id))
        return jsonify(status='error', reason='already_compensated')

    print('calling send_kin: %s, %s' % (public_address, kin_amount))
    try:
        tx_hash = send_kin(public_address, kin_amount, memo)
        create_tx(tx_hash, user_id, public_address, False, kin_amount, {
            'task_id': task_id,
            'memo': memo
        })
    except Exception as e:
        print('error attempting to compensate user %s for task %s' %
              (user_id, task_id))
        print(e)
        return jsonify(status='error', reason='internal_error')
    else:
        print('compensated user %s with %s kins for task_id %s' %
              (user_id, kin_amount, task_id))
        # also send push to the user
        task_title = get_task_details(task_id)['title']
        send_compensated_push(user_id, kin_amount, task_title)
        increment_metric('manual-compensation')

        return jsonify(status='ok', tx_hash=tx_hash)
예제 #13
0
def onboard_user():
    '''creates a wallet for the user and deposits some xlms there'''
    # input sanity
    try:
        user_id = extract_header(request)
        public_address = request.get_json(silent=True).get(
            'public_address', None)
        if None in (public_address, user_id):
            raise InvalidUsage('bad-request')
    except Exception as e:
        raise InvalidUsage('bad-request')

    # ensure the user exists but does not have an account:
    onboarded = is_onboarded(user_id)
    if onboarded is True:
        raise InvalidUsage('user already has an account')
    elif onboarded is None:
        raise InvalidUsage('no such user exists')
    else:
        # create an account, provided none is already being created
        lock = redis_lock.Lock(app.redis, 'address:%s' % public_address)
        if lock.acquire(blocking=False):
            try:
                print('creating account with address %s and amount %s' %
                      (public_address, config.STELLAR_INITIAL_ACCOUNT_BALANCE))
                tx_id = create_account(public_address,
                                       config.STELLAR_INITIAL_ACCOUNT_BALANCE)
                if (tx_id):
                    set_onboarded(user_id, True)
                else:
                    raise InternalError('failed to create account at %s' %
                                        public_address)
            except Exception as e:
                print('exception trying to create account:%s' % e)
                raise InternalError('unable to create account')
            else:
                print('created account %s with txid %s' %
                      (public_address, tx_id))
            finally:
                lock.release()
        else:
            raise InvalidUsage(
                'already creating account for user_id: %s and address: %s' %
                (user_id, public_address))

        increment_metric('user_onboarded')
        return jsonify(status='ok')
예제 #14
0
def send_country_not_supported(user_id):
    """sends a push to the given userid to tell them their country isnt supported"""
    #  add cooldown with redis to this function.
    if not (app.redis.set('countrynot:%s' % str(user_id),
                          '',
                          ex=COUNTRY_NOT_SUPPORTED_PUSH_COOLDOWN_SECONDS,
                          nx=True)):
        # returns None if already exists
        return

    push_id = generate_push_id()
    push_type = 'country_not_supported'
    from kinappserver.models import get_user_push_data
    os_type, token, push_env = get_user_push_data(user_id)
    if token:
        if os_type == OS_ANDROID:
            increment_metric('country_not_supported-android')
            # return  # not supported yet
            print('sending country_not_supported push message to GCM user %s' %
                  user_id)
            push_send_gcm(
                token,
                gcm_payload(
                    push_type, push_id, {
                        'title':
                        'Oh no!',
                        'body':
                        "Kinit is currently not available in your country. We are continuing to grow, so check back again soon."
                    }), push_env)

        else:
            increment_metric('country_not_supported-ios')
            print(
                'sending country_not_supported push message to APNS user %s' %
                user_id)
            push_send_apns(
                token,
                apns_payload(
                    "Oh no!",
                    "Kinit is currently not available in your country. We are continuing to grow, so check back again soon.",
                    push_type, push_id), push_env)
    else:
        print(
            'not sending country_not_supported push to user_id %s: no token' %
            user_id)
    return
예제 #15
0
def book_offer_api():
    '''books an offer by a user'''
    payload = request.get_json(silent=True)
    try:
        user_id = extract_header(request)
        offer_id = payload.get('id', None)
        if None in (user_id, offer_id):
            raise InvalidUsage('invalid payload')
    except Exception as e:
        raise InvalidUsage('bad-request')
    order_id, error_code = create_order(user_id, offer_id)
    if order_id:
        increment_metric('offers_booked')
        return jsonify(status='ok', order_id=order_id)
    else:
        return jsonify(
            status='error',
            reason=errors_to_string(error_code)), status.HTTP_400_BAD_REQUEST
예제 #16
0
def skip_wait_endpoint():
    """sets the next task's timestamp to the past for the given user"""
    limit_to_acl()
    limit_to_password()

    try:
        payload = request.get_json(silent=True)
        user_id = payload.get('user_id', None)
        cat_id = payload.get('cat_id', None)
        next_ts = payload.get('next_ts', 1)  # optional
        if user_id is None:
            raise InvalidUsage('bad-request')
    except Exception as e:
        print(e)
        raise InvalidUsage('bad-request')
    else:
        store_next_task_results_ts(user_id, 'fake_task_id', next_ts, cat_id)

    increment_metric('skip-wait')
    return jsonify(status='ok')
예제 #17
0
def send_please_upgrade_push(user_id):
    """sends a push to the given userid to please upgrade"""
    #  add cooldown with redis to this function.
    if not (app.redis.set('plsupgr:%s' % str(user_id),
                          '',
                          ex=PLEASE_UPGRADE_COOLDOWN_SECONDS,
                          nx=True)):
        # returns None if already exists
        return

    push_id = generate_push_id()
    push_type = 'please_upgrade'
    from kinappserver.models import get_user_push_data
    os_type, token, push_env = get_user_push_data(user_id)
    if token:
        if os_type == OS_ANDROID:
            increment_metric('pleaseupgrade-android')
            #return  # not supported yet
            print('sending please-upgrade push message to GCM user %s' %
                  user_id)
            push_send_gcm(
                token,
                gcm_payload(
                    push_type, push_id, {
                        'title': '',
                        'body': "Please upgrade the app to get the next task"
                    }), push_env)

        else:
            increment_metric('pleaseupgrade-ios')
            print('sending please-upgrade push message to APNS user %s' %
                  user_id)
            push_send_apns(
                token,
                apns_payload("", "Please upgrade the app to get the next task",
                             push_type, push_id), push_env)
    else:
        print('not sending please-upgrade push to user_id %s: no token' %
              user_id)
    return
예제 #18
0
def send_kin(public_address, amount, memo=None):
    """send kins to an address"""

    #  sanity:
    if public_address in (None, ''):
        log.error('cant send kin to address: %s' % public_address)
        return False, None

    if amount is None or amount < 1:
        log.error('cant send kin amount: %s' % amount)
        return False, None

    print('sending kin to address: %s' % public_address)  #TODO REMOVE
    from stellar_base.asset import Asset
    try:
        kin_asset = Asset(ASSET_NAME, config.STELLAR_KIN_ISSUER_ADDRESS)
        return app.kin_sdk._send_asset(kin_asset, public_address, amount, memo)
    except Exception as e:
        increment_metric('send_kin_error')
        print('caught exception sending %s kin to address %s' %
              (amount, public_address))
        print(e)
예제 #19
0
def order_gift_cards(merchant_code, merchant_template_id, denomination, num_of_cards=1):
    """this function orders a set of gift cards"""
    card_ids = []

    # get the creds object
    creds = get_bh_creds()
    if not creds or not creds['token']:
        print('no bh creds object/auth token')
        return False
    else:
        token = creds['token']

    order_id = egift_start_order_api(token, merchant_code, merchant_template_id)
    if not order_id:
        log.error('failed to create order')
        return False

    for i in range(num_of_cards):
        card_id = egift_add_card_api(token, order_id, denomination)
        if not card_id:
            log.error('failed to create card')
            return False
        else:
            print('order_gift_cards: added a bh card with id: %s' % card_id)
            card_ids.append(card_id)

    returned_order_id = egift_complete_order_api(token, order_id, creds['account_id'], creds['digital_signature'])
    if not returned_order_id:
        log.error('failed to complete order_id %s' % order_id)

    # at this point, the card isn't ready yet. we need to monitor the order
    # until it is processed. in the meanwhile, lets store it in the db.

    for card_id in card_ids:
        create_bh_card(card_id, order_id, merchant_code, denomination)
        increment_metric('bh_card_ordered')

    return True
예제 #20
0
def limit_to_acl(return_bool=False):
    """aborts unauthorized requests for sensitive APIs (nginx specific). allow on DEBUG

    the optional 'return_bool' flag governs whether the function aborts the request (default) or
    just returns a boolean.
    """
    source_ip = request.headers.get('X-Forwarded-For', None)
    if not source_ip:
        print('missing expected header')
        if return_bool:
            return False
        increment_metric('not-in-acl')
        abort(403)

    if not is_in_acl(source_ip):
        if return_bool:
            return False
        print('%s is not in ACL, rejecting' % source_ip)
        increment_metric('not-in-acl')
        abort(403)

    if return_bool:
        return True
예제 #21
0
def track_orders():
    """go over all the unprocessed orders and determine whether they were processed.
    if so, retrieve the redemption code for each of the cards in the now-processed orders.

    returns the number of yet unprocessed orders or -1 if the token was bad / sth went wrong
    """

    creds = get_bh_creds()
    token = creds['token']
    if not token:
        print('track_orders: no bh auth token')
        return -1

    orders_dict = list_unprocessed_orders()
    unprocessed_orders = 0

    for order_id in orders_dict.keys():
        time.sleep(0.1)  # dont choke their api servers
        status = get_order_status_api(token, order_id)
        print('received status:%s for order_id:%s' % (status, order_id))
        if status == 'incomplete':
            print('ignoring order_id %s with status: incomplete' % order_id)
            continue
        if status == 'cancelled':
            print('ignoring order_id %s with status: cancelled' % order_id)
            continue
        if status == 'processed':
            # order was processed, so add all related cards to our db
            print('detected a processed order: %s. getting card codes for this order...' % order_id)
            for card_id in orders_dict[order_id]:
                card = egift_get_card_api(token, card_id)
                if card is None:
                    print('could not retrieve info about cardid %s' % card_id)
                    increment_metric('bh_card_processing_failure')
                    continue
                print('card info: %s' % card)
                code = card['redemption_details']['activation_account_number']  # the actual redemption code is called 'activation_account_number'
                pin = card['redemption_details'].get('security_code', None)  # sometimes there's a PIN code - so add it if its there
                if pin:
                    code = code + '   PIN:%s' % pin
                merchant_code = card['merchant_code']
                card_id = card['id']
                order_id = card['order']['id']

                if create_good(merchant_code_to_offer_id(merchant_code, card_id, order_id), 'code', code, extra_info={'bh_card_id': card_id}):
                    print('created good with code %s for card_id %s' % (code, card_id))
                    increment_metric('bh_card_processed')
                else:
                    log.error('failed to convert bh card_id %s to good' % card_id)
                    increment_metric('bh_card_processing_failure')

            set_processed_orders(orders_dict[order_id])
        else:
            unprocessed_orders = unprocessed_orders + 1

    return unprocessed_orders
예제 #22
0
def store_task_results(user_id, task_id, results):
    """store the results provided by the user"""
    # reject hackers trying to send task results too soon

    try:
        # store the results

        try:
            user_task_results = UserTaskResults()
            user_task_results.user_id = user_id
            user_task_results.task_id = task_id
            user_task_results.results = results
            db.session.add(user_task_results)
            db.session.commit()
        except Exception as e:
            db.session.rollback()
            # this code handles the unlikely event that a user already had task results for this task, so rather
            # than INSERT, we UPDATE.
            log.info('store_task_results - failed to insert results. attempting to update instead. error:%s' % e)
            previous_task_results = UserTaskResults.query.filter_by(user_id=user_id).filter_by(task_id=task_id).first()
            previous_task_results.results = results
            db.session.add(previous_task_results)
            db.session.commit()
            log.info('store_task_results: overwritten user_id %s task %s results' % (user_id, task_id))
            increment_metric('overwrite-task-results')

        # write down the completed task-id
        from kinappserver.models import UserAppData
        user_app_data = UserAppData.query.filter_by(user_id=user_id).first()
        if user_app_data is None:
            log.error('cant retrieve user app data for user:%s' % user_id)
            raise InternalError('cant retrieve user app data for user:%s' % user_id)

        cat_id = get_cat_id_for_task_id(task_id)
        if not cat_id:
            log.error('cant find cat_id for task_id %s' % task_id)
            raise InternalError('cant find cat_id for task_id %s' % task_id)

        if cat_id in user_app_data.completed_tasks_dict:
            if task_id not in user_app_data.completed_tasks_dict[cat_id]:
                user_app_data.completed_tasks_dict[cat_id].append(task_id)
        else:
            user_app_data.completed_tasks_dict[cat_id] = [task_id]

        commit_json_changed_to_orm(user_app_data, ['completed_tasks_dict'])

        log.info('wrote user_app_data.completed_tasks for userid: %s' % user_id)

        # calculate the next valid submission time, and store it:
        delay_days = None
        # calculate the next task's valid submission time, and store it:
        # this takes into account the delay_days field on the next task.

        # note: even if we end up skipping the next task (for example, truex for iOS),
        # we should still use the original delay days value (as done here).

        try:
            delay_days = get_next_task_delay_days(user_id, task_id)  # throws exception if no such task exists
            print('next task delay:%s (user_id: %s, current task: %s)' % (delay_days, user_id, task_id))
        except Exception as e:
            log.error('cant find task_delay for next task_id of %s' % task_id)

        if delay_days is None or int(delay_days) == 0:
            shifted_ts = arrow.utcnow().timestamp
            log.info('setting next task time to now (delay_days is: %s)' % delay_days)
        else:
            shift_seconds = calculate_timeshift(user_id, delay_days)
            shifted_ts = arrow.utcnow().shift(seconds=shift_seconds).timestamp
            log.info('setting next task time to %s seconds in the future' % shift_seconds)

        log.info('next valid submission time for user %s, (previous task id %s) in shifted_ts: %s' % (user_id, task_id, shifted_ts))

        store_next_task_results_ts(user_id, task_id, shifted_ts)

        return True
    except Exception as e:
        log.error('exception in store_task_results: %s', e)
        raise InvalidUsage('cant store_task_results')