Exemplo n.º 1
0
def finish_payday():
    # 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.")
Exemplo n.º 2
0
def transfer(tipper, tippee, amount):
    """Given two unicodes and a Decimal, return a boolean indicating success.

    If the tipper doesn't have enough in their Gittip account then we return
    False. Otherwise we decrement tipper's balance and increment tippee's
    *pending* balance by amount.

    """
    typecheck(tipper, unicode, tippee, unicode, amount, decimal.Decimal)
    with db.get_connection() as conn:
        cursor = conn.cursor()

        try:
            debit_participant(cursor, tipper, amount)
        except ValueError:
            return False

        credit_participant(cursor, tippee, amount)
        record_transfer(cursor, tipper, tippee, amount)
        increment_payday(cursor, amount)

        # Success.
        # ========

        conn.commit()
        return True
Exemplo n.º 3
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.")
Exemplo n.º 4
0
def transfer(tipper, tippee, amount):
    """Given two unicodes and a Decimal, return a boolean indicating success.

    If the tipper doesn't have enough in their Gittip account then we return
    False. Otherwise we decrement tipper's balance and increment tippee's
    *pending* balance by amount.

    """
    typecheck(tipper, unicode, tippee, unicode, amount, decimal.Decimal)
    with db.get_connection() as conn:
        cursor = conn.cursor()

        # Decrement the tipper's balance.
        # ===============================

        DECREMENT = """\

           UPDATE participants
              SET balance=(balance - %s)
            WHERE id=%s
              AND pending IS NOT NULL
        RETURNING balance

        """
        cursor.execute(DECREMENT, (amount, tipper))
        rec = cursor.fetchone()
        assert rec is not None, (tipper, tippee, amount)  # sanity check
        if rec['balance'] < 0:

            # User is out of money. Bail. The transaction will be rolled back 
            # by our context manager.

            return False


        # Increment the tippee's *pending* balance.
        # =========================================
        # The pending balance will clear to the balance proper when Payday is 
        # done.

        INCREMENT = """\

           UPDATE participants
              SET pending=(pending + %s)
            WHERE id=%s
              AND pending IS NOT NULL
        RETURNING pending

        """
        cursor.execute(INCREMENT, (amount, tippee))
        rec = cursor.fetchone()
        assert rec is not None, (tipper, tippee, amount)  # sanity check


        # Record the transfer.
        # ====================

        RECORD = """\

          INSERT INTO transfers
                      (tipper, tippee, amount)
               VALUES (%s, %s, %s)

        """
        cursor.execute(RECORD, (tipper, tippee, amount))


        # Record some stats.
        # ==================

        STATS = """\

            UPDATE paydays 
               SET ntransfers = ntransfers + 1
                 , transfer_volume = transfer_volume + %s
             WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
         RETURNING id

        """
        cursor.execute(STATS, (amount,))
        assert_one_payday(cursor.fetchone())


        # Success.
        # ========
        
        conn.commit()
        return True
Exemplo n.º 5
0
    try:
        stripe.Charge.create( customer=stripe_customer_id
                            , amount=cents
                            , description=participant_id
                            , currency="USD"
                             )
        err = False
        log(msg + "succeeded.")
    except stripe.StripeError, err:
        log(msg + "failed: %s" % err.message)

    # XXX If the power goes out at this point then Postgres will be out of sync
    # with Stripe. We'll have to resolve that manually be reviewing the Stripe
    # transaction log and modifying Postgres accordingly.

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

        if err:
            last_bill_result = json.dumps(err.json_body)
            amount = decimal.Decimal('0.00')

            STATS = """\

                UPDATE paydays 
                   SET ncc_failing = ncc_failing + 1
                 WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
             RETURNING id
            
            """
            cur.execute(STATS)
Exemplo n.º 6
0
def charge(participant_id, pmt, amount):
    """Given two unicodes and a Decimal, return a boolean indicating success.

    This is the only place where we actually charge credit cards. Amount should
    be the nominal amount. We compute Gittip's fee in this function and add
    it to amount.

    """
    typecheck( pmt, (unicode, None)
             , participant_id, unicode
             , amount, decimal.Decimal
              )

    if pmt is None:
        STATS = """\

            UPDATE paydays 
               SET npmt_missing = npmt_missing + 1
             WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
         RETURNING id
        
        """
        assert_one_payday(db.fetchone(STATS))
        return False 


    # We have a purported payment method token. Try to use it.
    # ========================================================

    charge_amount = (amount + FEE[0]) * FEE[1]
    charge_amount = charge_amount.quantize(FEE[0], rounding=decimal.ROUND_UP)
    fee = charge_amount - amount
    log("Charging %s $%s + $%s fee = $%s." 
       % (participant_id, amount, fee, charge_amount))
    transaction = Processor.purchase(pmt, charge_amount, custom=participant_id)

    # XXX If the power goes out at this point then Postgres will be out of sync
    # with Samurai. We'll have to resolve that manually be reviewing the
    # Samurai transaction log and modifying Postgres accordingly.

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

        if transaction.errors:
            last_bill_result = json.dumps(transaction.errors)
            amount = decimal.Decimal('0.00')

            STATS = """\

                UPDATE paydays 
                   SET npmt_failing = npmt_failing + 1
                 WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
             RETURNING id
            
            """
            cur.execute(STATS)
            assert_one_payday(cur.fetchone())

        else:
            last_bill_result = ''

            EXCHANGE = """\

            INSERT INTO exchanges
                   (amount, fee, participant_id)
            VALUES (%s, %s, %s)

            """
            cur.execute(EXCHANGE, (amount, fee, participant_id))

            STATS = """\

                UPDATE paydays 
                   SET nexchanges = nexchanges + 1
                     , exchange_volume = exchange_volume + %s
                     , exchange_fees_volume = exchange_fees_volume + %s
                 WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
             RETURNING id
            
            """
            cur.execute(STATS, (charge_amount, fee))
            assert_one_payday(cur.fetchone())


        # Update the participant's balance.
        # =================================
        # Credit card charges go immediately to balance, not to pending.

        RESULT = """\

        UPDATE participants
           SET last_bill_result=%s 
             , balance=(balance + %s)
         WHERE id=%s

        """
        cur.execute(RESULT, (last_bill_result, amount, participant_id))

        conn.commit()

    return not bool(last_bill_result)  # True indicates success
Exemplo n.º 7
0
def charge(participant_id, balanced_account_uri, amount):
    """Given two unicodes and a Decimal, return a boolean indicating success.

    This is the only place where we actually charge credit cards. Amount should
    be the nominal amount. We compute Gittip's fee in this function and add
    it to amount.

    """
    typecheck( participant_id, unicode
             , balanced_account_uri, (unicode, None)
             , amount, decimal.Decimal
              )

    if balanced_account_uri is None:
        mark_payday_missing_funding()
        return False

    (charge_amount,
     fee,
     error_message) = charge_balanced_account(participant_id,
                                              balanced_account_uri,
                                              amount)

    # XXX If the power goes out at this point then Postgres will be out of sync
    # with Balanced. We'll have to resolve that manually be reviewing the
    # Balanced transaction log and modifying Postgres accordingly.
    #
    # this could be done by generating an ID locally and commiting that to the
    # db and then passing that through in the meta field -
    # https://www.balancedpayments.com/docs/meta
    # Then syncing would be a case of simply:
    # for payment in unresolved_payments:
    #     payment_in_balanced = balanced.Transaction.query.filter(
    #       **{'meta.unique_id': 'value'}).one()
    #     payment.transaction_uri = payment_in_balanced.uri
    #
    with db.get_connection() as conn:
        cur = conn.cursor()

        if error_message:
            last_bill_result = error_message
            amount = decimal.Decimal('0.00')
            mark_payday_failed(cur)
        else:
            last_bill_result = ''
            mark_payday_success(participant_id,
                                  amount,
                                  fee,
                                  charge_amount,
                                  cur)

        # Update the participant's balance.
        # =================================
        # Credit card charges go immediately to balance, not to pending.

        RESULT = """\

        UPDATE participants
           SET last_bill_result=%s
             , balance=(balance + %s)
         WHERE id=%s

        """
        cur.execute(RESULT, (last_bill_result, amount, participant_id))
        conn.commit()

    return not bool(last_bill_result)  # True indicates success
Exemplo n.º 8
0
def charge(participant_id, pmt, amount):
    """Given two unicodes and a Decimal, return a boolean indicating success.

    This is the only place where we actually charge credit cards. Amount should
    be the nominal amount. We compute Gittip's fee in this function and add
    it to amount.

    """
    typecheck(pmt, (unicode, None), participant_id, unicode, amount,
              decimal.Decimal)

    if pmt is None:
        STATS = """\

            UPDATE paydays 
               SET npmt_missing = npmt_missing + 1
             WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
         RETURNING id
        
        """
        assert_one_payday(db.fetchone(STATS))
        return False

    # We have a purported payment method token. Try to use it.
    # ========================================================

    charge_amount = (amount + FEE[0]) * FEE[1]
    charge_amount = charge_amount.quantize(FEE[0], rounding=decimal.ROUND_UP)
    fee = charge_amount - amount
    log("Charging %s $%s + $%s fee = $%s." %
        (participant_id, amount, fee, charge_amount))
    transaction = Processor.purchase(pmt, charge_amount, custom=participant_id)

    # XXX If the power goes out at this point then Postgres will be out of sync
    # with Samurai. We'll have to resolve that manually be reviewing the
    # Samurai transaction log and modifying Postgres accordingly.

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

        if transaction.errors:
            last_bill_result = json.dumps(transaction.errors)
            amount = decimal.Decimal('0.00')

            STATS = """\

                UPDATE paydays 
                   SET npmt_failing = npmt_failing + 1
                 WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
             RETURNING id
            
            """
            cur.execute(STATS)
            assert_one_payday(cur.fetchone())

        else:
            last_bill_result = ''

            EXCHANGE = """\

            INSERT INTO exchanges
                   (amount, fee, participant_id)
            VALUES (%s, %s, %s)

            """
            cur.execute(EXCHANGE, (amount, fee, participant_id))

            STATS = """\

                UPDATE paydays 
                   SET nexchanges = nexchanges + 1
                     , exchange_volume = exchange_volume + %s
                     , exchange_fees_volume = exchange_fees_volume + %s
                 WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
             RETURNING id
            
            """
            cur.execute(STATS, (charge_amount, fee))
            assert_one_payday(cur.fetchone())

        # Update the participant's balance.
        # =================================
        # Credit card charges go immediately to balance, not to pending.

        RESULT = """\

        UPDATE participants
           SET last_bill_result=%s 
             , balance=(balance + %s)
         WHERE id=%s

        """
        cur.execute(RESULT, (last_bill_result, amount, participant_id))

        conn.commit()

    return not bool(last_bill_result)  # True indicates success
Exemplo n.º 9
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.")
Exemplo n.º 10
0
def transfer(tipper, tippee, amount):
    """Given two unicodes and a Decimal, return a boolean indicating success.

    If the tipper doesn't have enough in their Gittip account then we return
    False. Otherwise we decrement tipper's balance and increment tippee's
    *pending* balance by amount.

    """
    typecheck(tipper, unicode, tippee, unicode, amount, decimal.Decimal)
    with db.get_connection() as conn:
        cursor = conn.cursor()

        # Decrement the tipper's balance.
        # ===============================

        DECREMENT = """\

           UPDATE participants
              SET balance=(balance - %s)
            WHERE id=%s
              AND pending IS NOT NULL
        RETURNING balance

        """
        cursor.execute(DECREMENT, (amount, tipper))
        rec = cursor.fetchone()
        assert rec is not None, (tipper, tippee, amount)  # sanity check
        if rec['balance'] < 0:

            # User is out of money. Bail. The transaction will be rolled back
            # by our context manager.

            return False

        # Increment the tippee's *pending* balance.
        # =========================================
        # The pending balance will clear to the balance proper when Payday is
        # done.

        INCREMENT = """\

           UPDATE participants
              SET pending=(pending + %s)
            WHERE id=%s
              AND pending IS NOT NULL
        RETURNING pending

        """
        cursor.execute(INCREMENT, (amount, tippee))
        rec = cursor.fetchone()
        assert rec is not None, (tipper, tippee, amount)  # sanity check

        # Record the transfer.
        # ====================

        RECORD = """\

          INSERT INTO transfers
                      (tipper, tippee, amount)
               VALUES (%s, %s, %s)

        """
        cursor.execute(RECORD, (tipper, tippee, amount))

        # Record some stats.
        # ==================

        STATS = """\

            UPDATE paydays 
               SET ntransfers = ntransfers + 1
                 , transfer_volume = transfer_volume + %s
             WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz
         RETURNING id

        """
        cursor.execute(STATS, (amount, ))
        assert_one_payday(cursor.fetchone())

        # Success.
        # ========

        conn.commit()
        return True