示例#1
0
def authorize(participant_id, pmt):
    """Given two unicodes, return a dict.

    This function attempts to authorize the credit card details referenced by
    pmt. If the attempt succeeds we cancel the transaction. If it fails we log
    the failure. Even for failure we keep the payment_method_token, we don't
    reset it to None/NULL. It's useful for loading the previous (bad) credit
    card info from Samurai in order to prepopulate the form.

    """
    typecheck(pmt, unicode, participant_id, unicode)
    transaction = Processor.authorize(pmt, '1.00', custom=participant_id)
    if transaction.errors:
        last_bill_result = json.dumps(transaction.errors)
        out = dict(transaction.errors)
    else:
        transaction.reverse()
        last_bill_result = ''
        out = {}
        
    STANDING = """\

    UPDATE participants
       SET payment_method_token=%s
         , last_bill_result=%s 
     WHERE id=%s

    """
    db.execute(STANDING, (pmt, last_bill_result, participant_id))
    return out
def clear(thing, participant_id, balanced_account_uri):
    typecheck( thing, unicode
             , participant_id, unicode
             , balanced_account_uri, unicode
              )
    assert thing in ("credit card", "bank account"), thing


    # XXX Things in balanced cannot be deleted at the moment.
    # =======================================================
    # Instead we mark all valid cards as invalid which will restrict against
    # anyone being able to issue charges against them in the future.
    #
    # See: https://github.com/balanced/balanced-api/issues/22

    account = balanced.Account.find(balanced_account_uri)
    things = account.cards if thing == "credit card" else account.bank_accounts

    for _thing in things:
        if _thing.is_valid:
            _thing.is_valid = False
            _thing.save()

    CLEAR = """\

        UPDATE participants
           SET last_%s_result=NULL
         WHERE id=%%s

    """ % ("bill" if thing == "credit card" else "ach")
    db.execute(CLEAR, (participant_id,))
 def from_id(cls, participant_id):
     from gittip import db
     session = cls.load_session("id=%s", participant_id)
     session['session_token'] = uuid.uuid4().hex
     db.execute("UPDATE participants SET session_token=%s WHERE id=%s",
                (session['session_token'], participant_id))
     return cls(session)
def outbound(response):
    from gittip import db
    session = {}
    if 'user' in response.request.context:
        user = response.request.context['user']
        if not isinstance(user, User):
            raise Response(
                400, "If you define 'user' in a simplate it has to "
                "be a User instance.")
        session = user.session
    if not session:  # user is anonymous
        if 'session' not in response.request.headers.cookie:
            # no cookie in the request, don't set one on response
            return
        else:
            # expired cookie in the request, instruct browser to delete it
            response.headers.cookie['session'] = ''
            expires = 0
    else:  # user is authenticated
        response.headers['Expires'] = BEGINNING_OF_EPOCH  # don't cache
        response.headers.cookie['session'] = session['session_token']
        expires = session['session_expires'] = time.time() + TIMEOUT
        SQL = """
            UPDATE participants SET session_expires=%s WHERE session_token=%s
        """
        db.execute(SQL, (datetime.datetime.fromtimestamp(expires),
                         session['session_token']))

    cookie = response.headers.cookie['session']
    # I am not setting domain, because it is supposed to default to what we
    # want: the domain of the object requested.
    #cookie['domain']
    cookie['path'] = '/'
    cookie['expires'] = rfc822.formatdate(expires)
    cookie['httponly'] = "Yes, please."
示例#5
0
def authorize(participant_id, pmt):
    """Given two unicodes, return a dict.

    This function attempts to authorize the credit card details referenced by
    pmt. If the attempt succeeds we cancel the transaction. If it fails we log
    the failure. Even for failure we keep the payment_method_token, we don't
    reset it to None/NULL. It's useful for loading the previous (bad) credit
    card info from Samurai in order to prepopulate the form.

    """
    typecheck(pmt, unicode, participant_id, unicode)
    transaction = Processor.authorize(pmt, '1.00', custom=participant_id)
    if transaction.errors:
        last_bill_result = json.dumps(transaction.errors)
        out = dict(transaction.errors)
    else:
        transaction.reverse()
        last_bill_result = ''
        out = {}

    STANDING = """\

    UPDATE participants
       SET payment_method_token=%s
         , last_bill_result=%s 
     WHERE id=%s

    """
    db.execute(STANDING, (pmt, last_bill_result, participant_id))
    return out
def get_balanced_account(participant_id, balanced_account_uri):
    """Find or create a balanced.Account.
    """
    typecheck( participant_id, unicode
             , balanced_account_uri, (unicode, None)
              )

    # XXX Balanced requires an email address
    # https://github.com/balanced/balanced-api/issues/20

    email_address = '{}@gittip.com'.format(participant_id)

    if balanced_account_uri is None:
        try:
            account = \
               balanced.Account.query.filter(email_address=email_address).one()
        except balanced.exc.NoResultFound:
            account = balanced.Account(email_address=email_address).save()
        BALANCED_ACCOUNT = """\

                UPDATE participants
                   SET balanced_account_uri=%s
                 WHERE id=%s

        """
        db.execute(BALANCED_ACCOUNT, (account.uri, participant_id))
        account.meta['participant_id'] = participant_id
        account.save()  # HTTP call under here
    else:
        account = balanced.Account.find(balanced_account_uri)
    return account
示例#7
0
def clear(participant_id, stripe_customer_id):
    typecheck(participant_id, unicode, stripe_customer_id, unicode)

    # "Unlike other objects, deleted customers can still be retrieved through
    # the API, in order to be able to track the history of customers while
    # still removing their credit card details and preventing any further
    # operations to be performed" https://stripe.com/docs/api#delete_customer
    #
    # Hmm ... should we protect against that in associate (above)?
    # 
    # What this means though is (I think?) that we'll continue to be able to
    # search for customers in the Stripe management UI by participant_id (which
    # is stored as description in associate) even after the association is lost
    # in our own database. This should be helpful for customer support.

    customer = stripe.Customer.retrieve(stripe_customer_id)
    customer.delete()

    CLEAR = """\

        UPDATE participants
           SET stripe_customer_id=NULL
             , last_bill_result=NULL
         WHERE id=%s

    """
    db.execute(CLEAR, (participant_id,))
 def from_id(cls, participant_id):
     from gittip import db
     session = cls.load_session("id=%s", participant_id)
     session['session_token'] = uuid.uuid4().hex
     db.execute( "UPDATE participants SET session_token=%s WHERE id=%s"
               , (session['session_token'], participant_id)
                )
     return cls(session)
示例#9
0
def set_as_claimed(participant_id):
    CLAIMED = """\

        UPDATE participants
           SET claimed_time=CURRENT_TIMESTAMP
         WHERE id=%s
           AND claimed_time IS NULL

    """
    db.execute(CLAIMED, (participant_id,))
示例#10
0
def set_as_claimed(participant_id):
    CLAIMED = """\

        UPDATE participants
           SET claimed_time=CURRENT_TIMESTAMP
         WHERE id=%s
           AND claimed_time IS NULL

    """
    db.execute(CLAIMED, (participant_id, ))
示例#11
0
def store_error(participant_id, msg):
    typecheck(participant_id, unicode, msg, unicode)
    ERROR = """\

        UPDATE participants
           SET last_bill_result=%s
         WHERE id=%s

    """
    db.execute(ERROR, (msg, participant_id))
示例#12
0
def store_error(thing, participant_id, msg):
    typecheck(thing, unicode, participant_id, unicode, msg, unicode)
    assert thing in ("credit card", "bank account"), thing
    ERROR = """\

        UPDATE participants
           SET last_%s_result=%%s
         WHERE id=%%s

    """ % ("bill" if thing == "credit card" else "ach")
    db.execute(ERROR, (msg, participant_id))
示例#13
0
def clear(participant_id, pmt):
    redact_pmt(pmt)
    CLEAR = """\

        UPDATE participants
           SET payment_method_token=NULL
             , last_bill_result=NULL
         WHERE id=%s

    """
    db.execute(CLEAR, (participant_id,))
示例#14
0
def clear(participant_id, pmt):
    redact_pmt(pmt)
    CLEAR = """\

        UPDATE participants
           SET payment_method_token=NULL
             , last_bill_result=NULL
         WHERE id=%s

    """
    db.execute(CLEAR, (participant_id, ))
示例#15
0
def associate(participant_id, stripe_customer_id, tok):
    """Given three unicodes, return a dict.

    This function attempts to associate the credit card details referenced by
    tok with a Stripe Customer. If the attempt succeeds we cancel the
    transaction. If it fails we log the failure. Even for failure we keep the
    payment_method_token, we don't reset it to None/NULL. It's useful for
    loading the previous (bad) credit card info from Stripe in order to
    prepopulate the form.

    """
    typecheck( participant_id, unicode
             , stripe_customer_id, (unicode, None)
             , tok, unicode
              )


    # Load or create a Stripe Customer.
    # =================================

    if stripe_customer_id is None:
        customer = stripe.Customer.create()
        CUSTOMER = """\
                
                UPDATE participants 
                   SET stripe_customer_id=%s 
                 WHERE id=%s
                
        """
        db.execute(CUSTOMER, (customer.id, participant_id))
        customer.description = participant_id
        customer.save()  # HTTP call under here
    else:
        customer = stripe.Customer.retrieve(stripe_customer_id)



    # Associate the card with the customer.
    # =====================================
    # Handle errors. Return a unicode, a simple error message. If empty it
    # means there was no error. Yay! Store any raw error message from the
    # Stripe API in JSON format as last_bill_result. That may be helpful for
    # debugging at some point.

    customer.card = tok
    try:
        customer.save()
    except stripe.StripeError, err:
        last_bill_result = json.dumps(err.json_body)
        typecheck(last_bill_result, str)
        out = err.message
示例#16
0
def initialize_payday():
    # Start Payday.
    # =============
    # We try to start a new Payday. If there is a Payday that hasn't finished
    # yet, then the UNIQUE constraint on ts_end will kick in and notify us
    # of that. In that case we load the existing Payday and work on it some
    # more. We use the start time of the current Payday to synchronize our
    # work.

    try:
        rec = db.fetchone("INSERT INTO paydays DEFAULT VALUES "
                          "RETURNING ts_start")
        log("Starting a new payday.")
    except IntegrityError:  # Collision, we have a Payday already.
        rec = db.fetchone("""
            SELECT ts_start
            FROM paydays
            WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
        """)
        log("Picking up with an existing payday.")
    assert rec is not None  # Must either create or recycle a Payday.
    payday_start = rec['ts_start']
    log("Payday started at %s." % payday_start)

    START_PENDING = """\

        UPDATE participants
           SET pending=0.00
         WHERE pending IS NULL

    """
    db.execute(START_PENDING)
    log("Zeroed out the pending column.")

    PARTICIPANTS = """\
        SELECT id, balance, balanced_account_uri
          FROM participants
         WHERE claimed_time IS NOT NULL
    """
    participants = db.fetchall(PARTICIPANTS)
    log("Fetched participants.")

    return participants, payday_start
示例#17
0
def associate(thing, participant_id, balanced_account_uri, balanced_thing_uri):
    """Given four unicodes, return a unicode.

    This function attempts to associate the credit card or bank account details
    referenced by balanced_thing_uri with a Balanced Account. If it fails we
    log and return a unicode describing the failure. Even for failure we keep
    balanced_account_uri; we don't reset it to None/NULL. It's useful for
    loading the previous (bad) info from Balanced in order to prepopulate the
    form.

    """
    typecheck( participant_id, unicode
             , balanced_account_uri, (unicode, None, balanced.Account)
             , balanced_thing_uri, unicode
             , thing, unicode
              )

    if isinstance(balanced_account_uri, balanced.Account):
        balanced_account = balanced_account_uri
    else:
        balanced_account = get_balanced_account( participant_id
                                               , balanced_account_uri
                                                )
    SQL = "UPDATE participants SET last_%s_result=%%s WHERE id=%%s"

    if thing == "credit card":
        add = balanced_account.add_card
        SQL %= "bill"
    else:
        assert thing == "bank account", thing # sanity check
        add = balanced_account.add_bank_account
        SQL %= "ach"

    try:
        add(balanced_thing_uri)
    except balanced.exc.HTTPError as err:
        error = err.message.decode('UTF-8')  # XXX UTF-8?
    else:
        error = ''
    typecheck(error, unicode)

    db.execute(SQL, (error, participant_id))
    return error
示例#18
0
def get_a_participant_id():
    """Return a random participant_id.

    The returned value is guaranteed to have been reserved in the database.

    """
    seatbelt = 0
    while 1:
        participant_id = hex(int(random.random() * 16**12))[2:].zfill(12)
        try:
            db.execute("INSERT INTO participants (id) VALUES (%s)",
                       (participant_id, ))
        except IntegrityError:  # Collision, try again with another value.
            seatbelt += 1
            if seatbelt > 100:
                raise RunawayTrain
        else:
            break

    return participant_id
示例#19
0
def clear(participant_id, balanced_account_uri):
    typecheck(participant_id, unicode, balanced_account_uri, unicode)

    # accounts in balanced cannot be deleted at the moment. instead we mark all
    # valid cards as invalid which will restrict against anyone being able to
    # issue charges against them in the future.
    customer = balanced.Account.find(balanced_account_uri)
    for card in customer.cards:
        if card.is_valid:
            card.is_valid = False
            card.save()

    CLEAR = """\

        UPDATE participants
           SET balanced_account_uri=NULL
             , last_bill_result=NULL
         WHERE id=%s

    """
    db.execute(CLEAR, (participant_id,))
示例#20
0
def get_a_participant_id():
    """Return a random participant_id.

    The returned value is guaranteed to have been reserved in the database.

    """
    seatbelt = 0
    while 1:
        participant_id = hex(int(random.random() * 16**12))[2:].zfill(12)
        try:
            db.execute( "INSERT INTO participants (id) VALUES (%s)"
                      , (participant_id,)
                       )
        except IntegrityError:  # Collision, try again with another value.
            seatbelt += 1
            if seatbelt > 100:
                raise RunawayTrain
        else:
            break

    return participant_id
示例#21
0
def test_get_amount_and_total_back_from_api():
    "Test that we get correct amounts and totals back on POSTs to tip.json"
    client = TestClient()

    # First, create some test data
    # We need accounts
    db.execute(CREATE_ACCOUNT, ("test_tippee1",))
    db.execute(CREATE_ACCOUNT, ("test_tippee2",))
    db.execute(CREATE_ACCOUNT, ("test_tipper",))

    # We need to get ourselves a token!
    response = client.get('/')
    csrf_token = response.request.context['csrf_token']

    # Then, add a $1.50 and $3.00 tip
    response1 = client.post("/test_tippee1/tip.json",
                            {'amount': "1.00", 'csrf_token': csrf_token},
                            user='******')

    response2 = client.post("/test_tippee2/tip.json",
                            {'amount': "3.00", 'csrf_token': csrf_token},
                            user='******')

    # Confirm we get back the right amounts.
    first_data = json.loads(response1.body)
    second_data = json.loads(response2.body)
    assert_equal(first_data['amount'], "1.00")
    assert_equal(first_data['total_giving'], "1.00")
    assert_equal(second_data['amount'], "3.00")
    assert_equal(second_data['total_giving'], "4.00")
示例#22
0
def claim_id(participant_id):
    """Given a participant_id, return a participant_id.

    If we can claim the given participant_id, we will. Otherwise we'll find a
    random one that isn't taken yet. Whichever we return is guaranteed to be 
    claimed in the database.

    """
    seatbelt = 0
    while 1:
        try:
            db.execute("INSERT INTO participants (id) VALUES (%s)",
                       (participant_id, ))
        except IntegrityError:  # Collision, try again with a random value.
            participant_id = hex(int(random.random() * 16**12))[2:].zfill(12)
            seatbelt += 1
            if seatbelt > 100:
                raise RunawayTrain
        else:
            break

    return participant_id
示例#23
0
def claim_id(participant_id):
    """Given a participant_id, return a participant_id.

    If we can claim the given participant_id, we will. Otherwise we'll find a
    random one that isn't taken yet. Whichever we return is guaranteed to be 
    claimed in the database.

    """
    seatbelt = 0
    while 1:
        try:
            db.execute( "INSERT INTO participants (id) VALUES (%s)"
                      , (participant_id,)
                       )
        except IntegrityError:  # Collision, try again with a random value.
            participant_id = hex(int(random.random() * 16**12))[2:].zfill(12)
            seatbelt += 1
            if seatbelt > 100:
                raise RunawayTrain
        else:
            break

    return participant_id
def outbound(response):
    from gittip import db
    session = {}
    if 'user' in response.request.context:
        user = response.request.context['user']
        if not isinstance(user, User):
            raise Response(400, "If you define 'user' in a simplate it has to "
                                "be a User instance.")
        session = user.session
    if not session:                                 # user is anonymous
        if 'session' not in response.request.headers.cookie:
            # no cookie in the request, don't set one on response
            return
        else:
            # expired cookie in the request, instruct browser to delete it
            response.headers.cookie['session'] = ''
            expires = 0
    else:                                           # user is authenticated
        response.headers['Expires'] = BEGINNING_OF_EPOCH # don't cache
        response.headers.cookie['session'] = session['session_token']
        expires = session['session_expires'] = time.time() + TIMEOUT
        SQL = """
            UPDATE participants SET session_expires=%s WHERE session_token=%s
        """
        db.execute( SQL
                  , ( datetime.datetime.fromtimestamp(expires)
                    , session['session_token']
                     )
                   )

    cookie = response.headers.cookie['session']
    # I am not setting domain, because it is supposed to default to what we
    # want: the domain of the object requested.
    #cookie['domain']
    cookie['path'] = '/'
    cookie['expires'] = rfc822.formatdate(expires)
    cookie['httponly'] = "Yes, please."
示例#25
0
def payday():
    """This is the big one.

    Settling the graph of Gittip balances is an abstract event called Payday.

    On Payday, we want to use a participant's Gittip balance to settle their
    tips due (pulling in more money via credit card as needed), but we only
    want to use their balance at the start of Payday. Balance changes should be
    atomic globally per-Payday.

    This function runs every Friday. It is structured such that it can be run 
    again safely if it crashes.
    
    """
    log("Greetings, program! It's PAYDAY!!!!")

    # Start Payday.
    # =============
    # We try to start a new Payday. If there is a Payday that hasn't finished
    # yet, then the UNIQUE constraint on ts_end will kick in and notify us
    # of that. In that case we load the existing Payday and work on it some
    # more. We use the start time of the current Payday to synchronize our
    # work.

    try:
        rec = db.fetchone("INSERT INTO paydays DEFAULT VALUES "
                          "RETURNING ts_start")
        log("Starting a new payday.")
    except IntegrityError:  # Collision, we have a Payday already.
        rec = db.fetchone(
            "SELECT ts_start FROM paydays WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz"
        )
        log("Picking up with an existing payday.")
    assert rec is not None  # Must either create or recycle a Payday.
    payday_start = rec['ts_start']
    log("Payday started at %s." % payday_start)

    START_PENDING = """\
        
        UPDATE participants
           SET pending=0.00
         WHERE pending IS NULL

    """
    db.execute(START_PENDING)
    log("Zeroed out the pending column.")

    PARTICIPANTS = """\
        SELECT id, balance, payment_method_token AS pmt
          FROM participants
    """
    participants = db.fetchall(PARTICIPANTS)
    log("Fetched participants.")

    # Drop to core.
    # =============
    # We are now locked for Payday. If the power goes out at this point then we
    # will need to start over and reacquire the lock.

    payday_loop(payday_start, participants)

    # Finish Payday.
    # ==============
    # Transfer pending into balance for all users, setting pending to NULL.
    # Close out the paydays entry as well.

    with db.get_connection() as conn:
        cursor = conn.cursor()

        cursor.execute("""\

            UPDATE participants
               SET balance = (balance + pending)
                 , pending = NULL

        """)
        cursor.execute("""\
            
            UPDATE paydays
               SET ts_end=now()
             WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
         RETURNING id

        """)
        assert_one_payday(cursor.fetchone())

        conn.commit()
        log("Finished payday.")
示例#26
0
def upsert(network, user_id, username, user_info, claim=False):
    """Given str, unicode, unicode, and dict, return unicode and boolean.

    Network is the name of a social network that we support (ASCII blah).
    User_id is an immutable unique identifier for the given user on the given
    social network. Username is the user's login/user_id on the given social
    network. We will try to claim that for them here on Gittip. If their
    username is already taken on Gittip then we give them a random one; they
    can change it on their Gittip profile page. User_id and username may or
    may not be the same. User is a dictionary of profile info per the named
    network. All network dicts must have an id key that corresponds to the
    primary key in the underlying table in our own db.

    If claim is True, the return value is the participant_id. Otherwise it is a
    tuple: (participant_id, claimed [boolean], balance).

    """
    typecheck( network, str
             , user_id, (int, unicode)
             , user_info, dict
              )  
    user_id = unicode(user_id)


    # Record the user info in our database.
    # =====================================

    INSERT = """\
            
        INSERT INTO social_network_users
                    (network, user_id) 
             VALUES (%s, %s)
             
    """ 
    try:
        db.execute(INSERT, (network, user_id,))
    except IntegrityError:
        pass  # That login is already in our db.
    
    UPDATE = """\
            
        UPDATE social_network_users
           SET user_info=%s
         WHERE user_id=%s 
     RETURNING participant_id

    """
    for k, v in user_info.items():
        # I believe hstore can take any type of value, but psycopg2 can't.
        # https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/
        # http://initd.org/psycopg/docs/extras.html#hstore-data-type
        user_info[k] = unicode(v)
    rec = db.fetchone(UPDATE, (user_info, user_id))


    # Find a participant.
    # ===================
    
    if rec is not None and rec['participant_id'] is not None:

        # There is already a Gittip participant associated with this account.

        participant_id = rec['participant_id']
        new_participant = False

    else:

        # This is the first time we've seen this user. Let's create a new
        # participant for them, claiming their user_id for them if possible.

        participant_id = claim_id(username)
        new_participant = True


    # Associate the social network user with the Gittip participant.
    # ================================================================

    ASSOCIATE = """\
            
        UPDATE social_network_users
           SET participant_id=%s
         WHERE network=%s
           AND user_id=%s
           AND (  (participant_id IS NULL)
               OR (participant_id=%s)
                 )
     RETURNING participant_id

    """

    log(u"Associating %s (%s) on %s with %s on Gittip." 
        % (username, user_id, network, participant_id))
    rows = db.fetchall( ASSOCIATE
                      , (participant_id, network, user_id, participant_id)
                       )
    nrows = len(list(rows))
    assert nrows in (0, 1)
    if nrows == 0:

        # Against all odds, the account was otherwise associated with another
        # participant while we weren't looking. Maybe someone paid them money
        # at *just* the right moment. If we created a new participant then back
        # that out.

        if new_participant:
            db.execute( "DELETE FROM participants WHERE id=%s"
                      , (participant_id,)
                       )

        rec = db.fetchone( "SELECT participant_id FROM social_network_users "
                           "WHERE network=%s AND user_id=%s" 
                         , (network, user_id)
                          )
        if rec is not None:

            # Use the participant associated with this account.

            participant_id = rec['participant_id']
            assert participant_id is not None

        else:

            # Okay, now this is just screwy. The participant disappeared right
            # at the last moment! Log it and fail.

            raise Exception("We're bailing on associating %s user %s (%s) with"
                            " a Gittip participant." 
                            % (network, username, user_id))


    # Record the participant as claimed if asked to.
    # ==============================================

    if claim:
        CLAIM = """\

            UPDATE participants 
               SET claimed_time=CURRENT_TIMESTAMP
             WHERE id=%s 
               AND claimed_time IS NULL

        """
        db.execute(CLAIM, (participant_id,))
        out = participant_id
    else:
        rec = db.fetchone( "SELECT claimed_time, balance FROM participants "
                           "WHERE id=%s"
                         , (participant_id,)
                          )
        assert rec is not None
        out = (participant_id, rec['claimed_time'] is not None, rec['balance'])

    return out
示例#27
0
 def suspend_payin(self):
     db.execute( "UPDATE participants SET payin_suspended=true WHERE id=%s"
               , (self.participant_id,)
                )
示例#28
0
        customer.save()
    except stripe.StripeError, err:
        last_bill_result = json.dumps(err.json_body)
        typecheck(last_bill_result, str)
        out = err.message
    else:
        out = last_bill_result = ''
        
    STANDING = """\

    UPDATE participants
       SET last_bill_result=%s 
     WHERE id=%s

    """
    db.execute(STANDING, (last_bill_result, participant_id))
    return out


def clear(participant_id, stripe_customer_id):
    typecheck(participant_id, unicode, stripe_customer_id, unicode)

    # "Unlike other objects, deleted customers can still be retrieved through
    # the API, in order to be able to track the history of customers while
    # still removing their credit card details and preventing any further
    # operations to be performed" https://stripe.com/docs/api#delete_customer
    #
    # Hmm ... should we protect against that in associate (above)?
    # 
    # What this means though is (I think?) that we'll continue to be able to
    # search for customers in the Stripe management UI by participant_id (which
示例#29
0
 def clear_paydays(self):
     "Clear all the existing paydays in the DB."
     from gittip import db
     db.execute("DELETE FROM paydays")
示例#30
0
def payday():
    """This is the big one.

    Settling the graph of Gittip balances is an abstract event called Payday.

    On Payday, we want to use a participant's Gittip balance to settle their
    tips due (pulling in more money via credit card as needed), but we only
    want to use their balance at the start of Payday. Balance changes should be
    atomic globally per-Payday.

    This function runs every Friday. It is structured such that it can be run 
    again safely if it crashes.
    
    """
    log("Greetings, program! It's PAYDAY!!!!")


    # Start Payday.
    # =============
    # We try to start a new Payday. If there is a Payday that hasn't finished 
    # yet, then the UNIQUE constraint on ts_end will kick in and notify us
    # of that. In that case we load the existing Payday and work on it some 
    # more. We use the start time of the current Payday to synchronize our 
    # work.

    try: 
        rec = db.fetchone("INSERT INTO paydays DEFAULT VALUES "
                          "RETURNING ts_start")
        log("Starting a new payday.")
    except IntegrityError:  # Collision, we have a Payday already.
        rec = db.fetchone("SELECT ts_start FROM paydays WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz")
        log("Picking up with an existing payday.")
    assert rec is not None  # Must either create or recycle a Payday.
    payday_start = rec['ts_start']
    log("Payday started at %s." % payday_start)

    START_PENDING = """\
        
        UPDATE participants
           SET pending=0.00
         WHERE pending IS NULL

    """
    db.execute(START_PENDING)
    log("Zeroed out the pending column.")

    PARTICIPANTS = """\
        SELECT id, balance, stripe_customer_id
          FROM participants
         WHERE claimed_time IS NOT NULL
    """
    participants = db.fetchall(PARTICIPANTS)
    log("Fetched participants.")
  

    # Drop to core.
    # =============
    # We are now locked for Payday. If the power goes out at this point then we
    # will need to start over and reacquire the lock.
    
    payday_loop(payday_start, participants)


    # Finish Payday.
    # ==============
    # Transfer pending into balance for all users, setting pending to NULL. 
    # Close out the paydays entry as well.

    with db.get_connection() as conn:
        cursor = conn.cursor()

        cursor.execute("""\

            UPDATE participants
               SET balance = (balance + pending)
                 , pending = NULL

        """)
        cursor.execute("""\
            
            UPDATE paydays
               SET ts_end=now()
             WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
         RETURNING id

        """)
        assert_one_payday(cursor.fetchone())

        conn.commit()
        log("Finished payday.")
示例#31
0
def upsert(network, user_id, username, user_info):
    """Given str, unicode, unicode, and dict, return unicode and boolean.

    Network is the name of a social network that we support (ASCII blah).
    User_id is an immutable unique identifier for the given user on the given
    social network. Username is the user's login/user_id on the given social
    network. It is only used here for logging. Specifically, we don't reserve
    their username for them on Gittip if they're new here. We give them a
    random participant_id here, and they'll have a chance to change it if/when
    they opt in. User_id and username may or may not be the same. User_info is
    a dictionary of profile info per the named network. All network dicts must
    have an id key that corresponds to the primary key in the underlying table
    in our own db.

    The return value is a tuple: (participant_id [unicode], is_claimed
    [boolean], is_locked [boolean], balance [Decimal]).

    """
    typecheck( network, str
             , user_id, (int, unicode)
             , username, unicode
             , user_info, dict
              )
    user_id = unicode(user_id)


    # Record the user info in our database.
    # =====================================

    INSERT = """\

        INSERT INTO social_network_users
                    (network, user_id)
             VALUES (%s, %s)

    """
    try:
        db.execute(INSERT, (network, user_id,))
    except IntegrityError:
        pass  # That login is already in our db.

    UPDATE = """\

        UPDATE social_network_users
           SET user_info=%s
         WHERE user_id=%s
     RETURNING participant_id

    """
    for k, v in user_info.items():
        # Cast everything to unicode. I believe hstore can take any type of
        # value, but psycopg2 can't.
        # https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/
        # http://initd.org/psycopg/docs/extras.html#hstore-data-type
        user_info[k] = unicode(v)
    rec = db.fetchone(UPDATE, (user_info, user_id))


    # Find a participant.
    # ===================

    if rec is not None and rec['participant_id'] is not None:

        # There is already a Gittip participant associated with this account.

        participant_id = rec['participant_id']
        new_participant = False

    else:

        # This is the first time we've seen this user. Let's create a new
        # participant for them.

        participant_id = get_a_participant_id()
        new_participant = True


    # Associate the social network user with the Gittip participant.
    # ================================================================

    ASSOCIATE = """\

        UPDATE social_network_users
           SET participant_id=%s
         WHERE network=%s
           AND user_id=%s
           AND (  (participant_id IS NULL)
               OR (participant_id=%s)
                 )
     RETURNING participant_id, is_locked

    """

    log(u"Associating %s (%s) on %s with %s on Gittip."
        % (username, user_id, network, participant_id))
    rows = db.fetchall( ASSOCIATE
                      , (participant_id, network, user_id, participant_id)
                       )
    rows = list(rows)
    nrows = len(rows)
    assert nrows in (0, 1)

    if nrows == 1:
        is_locked = rows[0]['is_locked']
    else:

        # Against all odds, the account was otherwise associated with another
        # participant while we weren't looking. Maybe someone paid them money
        # at *just* the right moment. If we created a new participant then back
        # that out.

        if new_participant:
            db.execute( "DELETE FROM participants WHERE id=%s"
                      , (participant_id,)
                       )

        rec = db.fetchone( "SELECT participant_id, is_locked "
                           "FROM social_network_users "
                           "WHERE network=%s AND user_id=%s"
                         , (network, user_id)
                          )
        if rec is not None:

            # Use the participant associated with this account.

            participant_id = rec['participant_id']
            is_locked = rec['is_locked']
            assert participant_id is not None

        else:

            # Okay, now this is just screwy. The participant disappeared right
            # at the last moment! Log it and fail.

            raise Exception("We're bailing on associating %s user %s (%s) with"
                            " a Gittip participant."
                            % (network, username, user_id))

    rec = db.fetchone( "SELECT claimed_time, balance FROM participants "
                       "WHERE id=%s"
                     , (participant_id,)
                      )
    assert rec is not None
    return ( participant_id
           , rec['claimed_time'] is not None
           , is_locked
           , rec['balance']
            )
示例#32
0
def associate(participant_id, balanced_account_uri, card_uri):
    """Given three unicodes, return a dict.

    This function attempts to associate the credit card details referenced by
    card_uri with a Balanced Account. If the attempt succeeds we cancel the
    transaction. If it fails we log the failure. Even for failure we keep the
    balanced_account_uri, we don't reset it to None/NULL. It's useful for
    loading the previous (bad) credit card info from Balanced in order to
    prepopulate the form.

    """
    typecheck( participant_id, unicode
             , balanced_account_uri, (unicode, None)
             , card_uri, unicode
              )


    # Load or create a Balanced Account.
    # ==================================

    email_address = '{}@gittip.com'.format(participant_id)
    if balanced_account_uri is None:
        # arg - balanced requires an email address
        try:
            customer = \
               balanced.Account.query.filter(email_address=email_address).one()
        except balanced.exc.NoResultFound:
            customer = balanced.Account(email_address=email_address).save()
        CUSTOMER = """\
                
                UPDATE participants 
                   SET balanced_account_uri=%s
                 WHERE id=%s
                
        """
        db.execute(CUSTOMER, (customer.uri, participant_id))
        customer.meta['participant_id'] = participant_id
        customer.save()  # HTTP call under here
    else:
        customer = balanced.Account.find(balanced_account_uri)


    # Associate the card with the customer.
    # =====================================
    # Handle errors. Return a unicode, a simple error message. If empty it
    # means there was no error. Yay! Store any error message from the
    # Balanced API as a string in last_bill_result. That may be helpful for
    # debugging at some point.

    customer.card_uri = card_uri
    try:
        customer.save()
    except balanced.exc.HTTPError as err:
        last_bill_result = err.message.decode('UTF-8')  # XXX UTF-8?
        typecheck(last_bill_result, unicode)
        out = last_bill_result
    else:
        out = last_bill_result = ''
        
    STANDING = """\

        UPDATE participants
           SET last_bill_result=%s 
         WHERE id=%s

    """
    db.execute(STANDING, (last_bill_result, participant_id))
    return out
示例#33
0
def upsert(network, user_id, username, user_info):
    """Given str, unicode, unicode, and dict, return unicode and boolean.

    Network is the name of a social network that we support (ASCII blah).
    User_id is an immutable unique identifier for the given user on the given
    social network. Username is the user's login/user_id on the given social
    network. It is only used here for logging. Specifically, we don't reserve
    their username for them on Gittip if they're new here. We give them a
    random participant_id here, and they'll have a chance to change it if/when
    they opt in. User_id and username may or may not be the same. User_info is
    a dictionary of profile info per the named network. All network dicts must
    have an id key that corresponds to the primary key in the underlying table
    in our own db.

    The return value is a tuple: (participant_id [unicode], is_claimed
    [boolean], is_locked [boolean], balance [Decimal]).

    """
    typecheck(network, str, user_id, (int, unicode), username, unicode,
              user_info, dict)
    user_id = unicode(user_id)

    # Record the user info in our database.
    # =====================================

    INSERT = """\

        INSERT INTO social_network_users
                    (network, user_id)
             VALUES (%s, %s)

    """
    try:
        db.execute(INSERT, (
            network,
            user_id,
        ))
    except IntegrityError:
        pass  # That login is already in our db.

    UPDATE = """\

        UPDATE social_network_users
           SET user_info=%s
         WHERE user_id=%s
     RETURNING participant_id

    """
    for k, v in user_info.items():
        # Cast everything to unicode. I believe hstore can take any type of
        # value, but psycopg2 can't.
        # https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/
        # http://initd.org/psycopg/docs/extras.html#hstore-data-type
        user_info[k] = unicode(v)
    rec = db.fetchone(UPDATE, (user_info, user_id))

    # Find a participant.
    # ===================

    if rec is not None and rec['participant_id'] is not None:

        # There is already a Gittip participant associated with this account.

        participant_id = rec['participant_id']
        new_participant = False

    else:

        # This is the first time we've seen this user. Let's create a new
        # participant for them.

        participant_id = get_a_participant_id()
        new_participant = True

    # Associate the social network user with the Gittip participant.
    # ================================================================

    ASSOCIATE = """\

        UPDATE social_network_users
           SET participant_id=%s
         WHERE network=%s
           AND user_id=%s
           AND (  (participant_id IS NULL)
               OR (participant_id=%s)
                 )
     RETURNING participant_id, is_locked

    """

    log(u"Associating %s (%s) on %s with %s on Gittip." %
        (username, user_id, network, participant_id))
    rows = db.fetchall(ASSOCIATE,
                       (participant_id, network, user_id, participant_id))
    rows = list(rows)
    nrows = len(rows)
    assert nrows in (0, 1)

    if nrows == 1:
        is_locked = rows[0]['is_locked']
    else:

        # Against all odds, the account was otherwise associated with another
        # participant while we weren't looking. Maybe someone paid them money
        # at *just* the right moment. If we created a new participant then back
        # that out.

        if new_participant:
            db.execute("DELETE FROM participants WHERE id=%s",
                       (participant_id, ))

        rec = db.fetchone(
            "SELECT participant_id, is_locked "
            "FROM social_network_users "
            "WHERE network=%s AND user_id=%s", (network, user_id))
        if rec is not None:

            # Use the participant associated with this account.

            participant_id = rec['participant_id']
            is_locked = rec['is_locked']
            assert participant_id is not None

        else:

            # Okay, now this is just screwy. The participant disappeared right
            # at the last moment! Log it and fail.

            raise Exception("We're bailing on associating %s user %s (%s) with"
                            " a Gittip participant." %
                            (network, username, user_id))

    rec = db.fetchone(
        "SELECT claimed_time, balance FROM participants "
        "WHERE id=%s", (participant_id, ))
    assert rec is not None
    return (participant_id, rec['claimed_time']
            is not None, is_locked, rec['balance'])
示例#34
0
def upsert(network, user_id, username, user_info, claim=False):
    """Given str, unicode, unicode, and dict, return unicode and boolean.

    Network is the name of a social network that we support (ASCII blah).
    User_id is an immutable unique identifier for the given user on the given
    social network. Username is the user's login/user_id on the given social
    network. We will try to claim that for them here on Gittip. If their
    username is already taken on Gittip then we give them a random one; they
    can change it on their Gittip profile page. User_id and username may or
    may not be the same. User is a dictionary of profile info per the named
    network. All network dicts must have an id key that corresponds to the
    primary key in the underlying table in our own db.

    If claim is True, the return value is the participant_id. Otherwise it is a
    tuple: (participant_id, claimed [boolean], balance).

    """
    typecheck(network, str, user_id, (int, unicode), user_info, dict)
    user_id = unicode(user_id)

    # Record the user info in our database.
    # =====================================

    INSERT = """\
            
        INSERT INTO social_network_users
                    (network, user_id) 
             VALUES (%s, %s)
             
    """
    try:
        db.execute(INSERT, (
            network,
            user_id,
        ))
    except IntegrityError:
        pass  # That login is already in our db.

    UPDATE = """\
            
        UPDATE social_network_users
           SET user_info=%s
         WHERE user_id=%s 
     RETURNING participant_id

    """
    for k, v in user_info.items():
        # I believe hstore can take any type of value, but psycopg2 can't.
        # https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/
        # http://initd.org/psycopg/docs/extras.html#hstore-data-type
        user_info[k] = unicode(v)
    rec = db.fetchone(UPDATE, (user_info, user_id))

    # Find a participant.
    # ===================

    if rec is not None and rec['participant_id'] is not None:

        # There is already a Gittip participant associated with this account.

        participant_id = rec['participant_id']
        new_participant = False

    else:

        # This is the first time we've seen this user. Let's create a new
        # participant for them, claiming their user_id for them if possible.

        participant_id = claim_id(username)
        new_participant = True

    # Associate the social network user with the Gittip participant.
    # ================================================================

    ASSOCIATE = """\
            
        UPDATE social_network_users
           SET participant_id=%s
         WHERE network=%s
           AND user_id=%s
           AND (  (participant_id IS NULL)
               OR (participant_id=%s)
                 )
     RETURNING participant_id

    """

    log(u"Associating %s (%s) on %s with %s on Gittip." %
        (username, user_id, network, participant_id))
    rows = db.fetchall(ASSOCIATE,
                       (participant_id, network, user_id, participant_id))
    nrows = len(list(rows))
    assert nrows in (0, 1)
    if nrows == 0:

        # Against all odds, the account was otherwise associated with another
        # participant while we weren't looking. Maybe someone paid them money
        # at *just* the right moment. If we created a new participant then back
        # that out.

        if new_participant:
            db.execute("DELETE FROM participants WHERE id=%s",
                       (participant_id, ))

        rec = db.fetchone(
            "SELECT participant_id FROM social_network_users "
            "WHERE network=%s AND user_id=%s", (network, user_id))
        if rec is not None:

            # Use the participant associated with this account.

            participant_id = rec['participant_id']
            assert participant_id is not None

        else:

            # Okay, now this is just screwy. The participant disappeared right
            # at the last moment! Log it and fail.

            raise Exception("We're bailing on associating %s user %s (%s) with"
                            " a Gittip participant." %
                            (network, username, user_id))

    # Record the participant as claimed if asked to.
    # ==============================================

    if claim:
        CLAIM = """\

            UPDATE participants 
               SET claimed_time=CURRENT_TIMESTAMP
             WHERE id=%s 
               AND claimed_time IS NULL

        """
        db.execute(CLAIM, (participant_id, ))
        out = participant_id
    else:
        rec = db.fetchone(
            "SELECT claimed_time, balance FROM participants "
            "WHERE id=%s", (participant_id, ))
        assert rec is not None
        out = (participant_id, rec['claimed_time'] is not None, rec['balance'])

    return out