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."
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
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)
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,))
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, ))
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))
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))
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,))
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, ))
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
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
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
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
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,))
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
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")
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 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."
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.")
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
def suspend_payin(self): db.execute( "UPDATE participants SET payin_suspended=true WHERE id=%s" , (self.participant_id,) )
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
def clear_paydays(self): "Clear all the existing paydays in the DB." from gittip import db db.execute("DELETE FROM paydays")
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.")
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'] )
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
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'])
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