def test_credit_card_page_shows_card_failing(self): ExchangeRoute.from_network(self.janet, 'balanced-cc').update_error('Some error') expected = 'Your credit card is <em id="status">failing' actual = self.client.GET('/janet/routes/credit-card.html', auth_as='janet').body.decode('utf8') assert expected in actual
def test_giving_is_updated_when_credit_card_fails(self, btd): alice = self.make_participant("alice", claimed_time="now", last_bill_result="") team = self.make_team(is_approved=True) alice.set_payment_instruction(team, "5.00") # funded assert alice.giving == Decimal("5.00") assert Team.from_slug(team.slug).receiving == Decimal("5.00") ExchangeRoute.from_network(alice, "braintree-cc").update_error("Card expired") assert Participant.from_username("alice").giving == Decimal("0.00") assert Team.from_slug(team.slug).receiving == Decimal("0.00")
def test_giving_is_updated_when_credit_card_is_updated(self, btd): alice = self.make_participant('alice', claimed_time='now', last_bill_result='fail') team = self.make_team(is_approved=True) alice.set_payment_instruction(team, '5.00') # Not funded, failing card assert alice.giving == Decimal('0.00') assert Team.from_slug(team.slug).receiving == Decimal('0.00') # Alice updates her card.. ExchangeRoute.from_network(alice, 'braintree-cc').invalidate() ExchangeRoute.insert(alice, 'braintree-cc', '/cards/bar', '') assert alice.giving == Decimal('5.00') assert Team.from_slug(team.slug).receiving == Decimal('5.00')
def test_giving_is_updated_when_credit_card_fails(self, btd): alice = self.make_participant('alice', claimed_time='now', last_bill_result='') team = self.make_team(is_approved=True) alice.set_payment_instruction(team, '5.00') # funded assert alice.giving == Decimal('5.00') assert Team.from_slug(team.slug).receiving == Decimal('5.00') assert Participant.from_username(team.owner).taking == Decimal('5.00') ExchangeRoute.from_network(alice, 'braintree-cc').update_error("Card expired") assert Participant.from_username('alice').giving == Decimal('0.00') assert Team.from_slug(team.slug).receiving == Decimal('0.00') assert Participant.from_username(team.owner).taking == Decimal('0.00')
def test_giving_is_updated_when_credit_card_is_updated(self, btd): alice = self.make_participant("alice", claimed_time="now", last_bill_result="fail") team = self.make_team(is_approved=True) alice.set_payment_instruction(team, "5.00") # Not funded, failing card assert alice.giving == Decimal("0.00") assert Team.from_slug(team.slug).receiving == Decimal("0.00") # Alice updates her card.. ExchangeRoute.from_network(alice, "braintree-cc").invalidate() ExchangeRoute.insert(alice, "braintree-cc", "/cards/bar", "") assert alice.giving == Decimal("5.00") assert Team.from_slug(team.slug).receiving == Decimal("5.00")
def test_credit_callback(self, rer): alice = self.make_participant('alice', last_ach_result='') ba = ExchangeRoute.from_network(alice, 'balanced-ba') for status in ('succeeded', 'failed'): error = 'FOO' if status == 'failed' else None e_id = record_exchange(self.db, ba, 10, 0, alice, 'pre') body = json.dumps({ "events": [{ "type": "credit." + status, "entity": { "credits": [{ "failure_reason": error, "meta": { "participant_id": alice.id, "exchange_id": e_id, }, "status": status, }] } }] }) r = self.callback(body=body, csrf_token=False) assert r.code == 200, r.body assert rer.call_count == 1 assert rer.call_args[0][:-1] == (self.db, e_id, status, error) assert rer.call_args[0][-1].id == alice.id assert rer.call_args[1] == {} rer.reset_mock()
def test_re_result_restores_balance_on_error(self): alice = self.make_participant('alice', balance=30, last_paypal_result='') ba = ExchangeRoute.from_network(alice, 'paypal') e_id = record_exchange(self.db, ba, D('-27.06'), D('0.81'), alice, 'pre') assert alice.balance == D('02.13') record_exchange_result(self.db, e_id, 'failed', 'SOME ERROR', alice) assert P('alice').balance == D('30.00')
def test_re_result_updates_balance_for_positive_amounts(self): alice = self.make_participant('alice', balance=4, last_bill_result='') cc = ExchangeRoute.from_network(alice, 'braintree-cc') e_id = record_exchange(self.db, cc, D('31.59'), D('0.01'), alice, 'pre') assert alice.balance == D('4.00') record_exchange_result(self.db, e_id, 'succeeded', None, alice) assert P('alice').balance == D('35.59')
def test_associate_paypal(self, mailer): mailer.return_value = 1 # Email successfully sent self.roman.add_email('*****@*****.**') self.db.run("UPDATE emails SET verified=true WHERE address='*****@*****.**'") self.hit('roman', 'associate', 'paypal', '*****@*****.**') assert ExchangeRoute.from_network(self.roman, 'paypal') assert self.roman.has_payout_route
def record_an_exchange(self, data, make_participants=True): if make_participants: self.make_participants() data.setdefault('status', 'succeeded') data.setdefault('note', 'noted') if 'route_id' not in data: try: data['route_id'] = ExchangeRoute.insert( self.bob, 'paypal', '*****@*****.**').id except IntegrityError: data['route_id'] = ExchangeRoute.from_network( self.bob, 'paypal').id if data['status'] is None: del (data['status']) if data['route_id'] is None: del (data['route_id']) if 'ref' not in data: data['ref'] = 'N/A' return self.client.PxST('/~bob/history/record-an-exchange', data, auth_as='alice')
def test_re_result_doesnt_restore_balance_on_success(self): alice = self.make_participant('alice', balance=50, last_paypal_result='') ba = ExchangeRoute.from_network(alice, 'paypal') e_id = record_exchange(self.db, ba, D('-43.98'), D('1.60'), alice, 'pre') assert alice.balance == D('4.42') record_exchange_result(self.db, e_id, 'succeeded', None, alice) assert P('alice').balance == D('4.42')
def test_credit_callback(self, rer): alice = self.make_participant('alice') ExchangeRoute.insert(alice, 'balanced-ba', '/bank/foo', '') ba = ExchangeRoute.from_network(alice, 'balanced-ba') for status in ('succeeded', 'failed'): error = 'FOO' if status == 'failed' else None e_id = record_exchange(self.db, ba, 10, 0, alice, 'pre') body = json.dumps({ "events": [ { "type": "credit."+status, "entity": { "credits": [ { "failure_reason": error, "meta": { "participant_id": alice.id, "exchange_id": e_id, }, "status": status, } ] } } ] }) r = self.callback(body=body, csrf_token=False) assert r.code == 200, r.body assert rer.call_count == 1 assert rer.call_args[0][:-1] == (self.db, e_id, status, error) assert rer.call_args[0][-1].id == alice.id assert rer.call_args[1] == {} rer.reset_mock()
def create_card_hold(db, participant, amount): """Create a hold on the participant's credit card. Amount should be the nominal amount. We'll compute Gratipay's fee below this function and add it to amount to end up with charge_amount. """ typecheck(amount, Decimal) username = participant.username # Perform some last-minute checks. # ================================ if participant.is_suspicious is not False: raise NotWhitelisted # Participant not trusted. route = ExchangeRoute.from_network(participant, 'braintree-cc') if not route: return None, 'No credit card' # Go to Braintree. # ================ cents, amount_str, charge_amount, fee = _prep_hit(amount) amount = charge_amount - fee msg = "Holding " + amount_str + " on Braintree for " + username + " ... " hold = None error = "" try: result = braintree.Transaction.sale({ 'amount': str(cents/100.0), 'customer_id': route.participant.braintree_customer_id, 'payment_method_token': route.address, 'options': { 'submit_for_settlement': False }, 'custom_fields': {'participant_id': participant.id} }) if result.is_success and result.transaction.status == 'authorized': error = "" hold = result.transaction elif result.is_success: error = "Transaction status was %s" % result.transaction.status else: error = result.message except Exception as e: error = repr_exception(e) if error == '': log(msg + "succeeded.") else: log(msg + "failed: %s" % error) record_exchange(db, route, amount, fee, participant, 'failed', error) return hold, error
def test_associate_paypal(self, mailer): mailer.return_value = 1 # Email successfully sent self.roman.add_email('*****@*****.**') self.db.run( "UPDATE emails SET verified=true WHERE address='*****@*****.**'") self.hit('roman', 'associate', 'paypal', '*****@*****.**') assert ExchangeRoute.from_network(self.roman, 'paypal') assert self.roman.has_payout_route
def credit_card_expiring(self): route = ExchangeRoute.from_network(self, 'braintree-cc') if not route: return card = CreditCard.from_route(route) year, month = card.expiration_year, card.expiration_month if not (year and month): return False return is_card_expiring(int(year), int(month))
def test_re_doesnt_update_balance_for_positive_amounts(self): alice = self.make_participant('alice', last_bill_result='') record_exchange(self.db, ExchangeRoute.from_network(alice, 'braintree-cc'), amount=D("0.59"), fee=D("0.41"), participant=alice, status='pre') assert P('alice').balance == D('0.00')
def test_associate_paypal_invalid(self): r = self.hit('roman', 'associate', 'paypal', '*****@*****.**', expected=400) assert not ExchangeRoute.from_network(self.roman, 'paypal') assert not self.roman.has_payout_route assert "Only verified email addresses allowed." in r.body
def test_associate_and_delete_valid_paypal(self): self.add_and_verify_email(self.roman, '*****@*****.**') self.hit('roman', 'associate', 'paypal', '*****@*****.**') assert ExchangeRoute.from_network(self.roman, 'paypal') assert self.roman.has_payout_route self.hit('roman', 'delete', 'paypal', '*****@*****.**') assert not self.roman.has_payout_route
def test_re_requires_valid_route(self): alice = self.make_participant('alice', last_bill_result='') bob = self.make_participant('bob', last_bill_result='') with self.assertRaises(AssertionError): record_exchange(self.db, ExchangeRoute.from_network(bob, 'braintree-cc'), amount=D("0.59"), fee=D("0.41"), participant=alice, status='pre')
def make_exchange(self, route, amount, fee, participant, status='succeeded', error=''): if not isinstance(route, ExchangeRoute): network = route route = ExchangeRoute.from_network(participant, network) if not route: route = ExchangeRoute.insert(participant, network, 'dummy-address') assert route e_id = record_exchange(self.db, route, amount, fee, participant, 'pre') record_exchange_result(self.db, e_id, status, error, participant) return e_id
def test_re_result_restores_balance_on_error_with_invalidated_route(self): alice = self.make_participant('alice', balance=37, last_paypal_result='') pp = ExchangeRoute.from_network(alice, 'paypal') e_id = record_exchange(self.db, pp, D('-32.45'), D('0.86'), alice, 'pre') assert alice.balance == D('3.69') pp.update_error('invalidated') record_exchange_result(self.db, e_id, 'failed', 'oops', alice) alice = P('alice') assert alice.balance == D('37.00') assert pp.error == alice.get_paypal_error() == 'invalidated'
def test_add_new_paypal_address(self): self.add_and_verify_email(self.roman, '*****@*****.**') self.add_and_verify_email(self.roman, '*****@*****.**') self.hit('roman', 'associate', 'paypal', '*****@*****.**') self.hit('roman', 'delete', 'paypal', '*****@*****.**') self.hit('roman', 'associate', 'paypal', '*****@*****.**') assert self.roman.has_payout_route assert ExchangeRoute.from_network(self.roman, 'paypal').address == '*****@*****.**'
def test_re_updates_balance_for_negative_amounts(self): alice = self.make_participant('alice', balance=50, last_paypal_result='') record_exchange( self.db , ExchangeRoute.from_network(alice, 'paypal') , amount=D('-35.84') , fee=D('0.75') , participant=alice , status='pre' ) assert P('alice').balance == D('13.41')
def test_record_exchange_result_restores_balance_on_error_with_invalidated_route(self): alice = self.make_participant('alice', balance=37, last_ach_result='') ba = ExchangeRoute.from_network(alice, 'balanced-ba') e_id = record_exchange(self.db, ba, D('-32.45'), D('0.86'), alice, 'pre') assert alice.balance == D('3.69') ba.update_error('invalidated') record_exchange_result(self.db, e_id, 'failed', 'oops', alice) alice = Participant.from_username('alice') assert alice.balance == D('37.00') assert ba.error == alice.get_bank_account_error() == 'invalidated'
def test_re_doesnt_update_balance_for_positive_amounts(self): alice = self.make_participant('alice', last_bill_result='') record_exchange( self.db , ExchangeRoute.from_network(alice, 'braintree-cc') , amount=D("0.59") , fee=D("0.41") , participant=alice , status='pre' ) assert P('alice').balance == D('0.00')
def test_re_requires_valid_route(self): alice = self.make_participant('alice', last_bill_result='') bob = self.make_participant('bob', last_bill_result='') with self.assertRaises(AssertionError): record_exchange( self.db , ExchangeRoute.from_network(bob, 'braintree-cc') , amount=D("0.59") , fee=D("0.41") , participant=alice , status='pre' )
def test_re_records_exchange(self): alice = self.make_participant('alice', last_bill_result='') record_exchange( self.db , ExchangeRoute.from_network(alice, 'braintree-cc') , amount=D("0.59") , fee=D("0.41") , participant=alice , status='pre' ) actual = self.db.one(""" SELECT amount, fee, participant, status, route FROM exchanges """, back_as=dict) expected = { "amount": D('0.59') , "fee": D('0.41') , "participant": "alice" , "status": 'pre' , "route": ExchangeRoute.from_network(alice, 'braintree-cc').id } assert actual == expected
def test_add_new_paypal_address(self): self.add_and_verify_email(self.roman, '*****@*****.**') self.add_and_verify_email(self.roman, '*****@*****.**') self.hit('roman', 'associate', 'paypal', '*****@*****.**') self.hit('roman', 'delete', 'paypal', '*****@*****.**') self.hit('roman', 'associate', 'paypal', '*****@*****.**') assert self.roman.has_payout_route assert ExchangeRoute.from_network( self.roman, 'paypal').address == '*****@*****.**'
def test_re_stores_error_in_note(self): alice = self.make_participant('alice', last_bill_result='') record_exchange(self.db, ExchangeRoute.from_network(alice, 'braintree-cc'), amount=D("0.59"), fee=D("0.41"), participant=alice, status='pre', error='Card payment failed') exchange = self.db.one("SELECT * FROM exchanges") assert exchange.note == 'Card payment failed'
def make_exchange(self, route, amount, fee, participant, status='succeeded', error=''): if not isinstance(route, ExchangeRoute): network = route route = ExchangeRoute.from_network(participant, network) if not route: from .balanced import BalancedHarness route = ExchangeRoute.insert(participant, network, BalancedHarness.card_href) assert route e_id = record_exchange(self.db, route, amount, fee, participant, 'pre') record_exchange_result(self.db, e_id, status, error, participant) return e_id
def test_record_exchange_result_restores_balance_on_error_with_invalidated_route( self): alice = self.make_participant('alice', balance=37, last_ach_result='') ba = ExchangeRoute.from_network(alice, 'balanced-ba') e_id = record_exchange(self.db, ba, D('-32.45'), D('0.86'), alice, 'pre') assert alice.balance == D('3.69') ba.update_error('invalidated') record_exchange_result(self.db, e_id, 'failed', 'oops', alice) alice = Participant.from_username('alice') assert alice.balance == D('37.00') assert ba.error == alice.get_bank_account_error() == 'invalidated'
def test_re_stores_error_in_note(self): alice = self.make_participant('alice', last_bill_result='') record_exchange( self.db , ExchangeRoute.from_network(alice, 'braintree-cc') , amount=D("0.59") , fee=D("0.41") , participant=alice , status='pre' , error='Card payment failed' ) exchange = self.db.one("SELECT * FROM exchanges") assert exchange.note == 'Card payment failed'
def make_exchange(self, route, amount, fee, participant, status='succeeded', error='', ref='dummy-trans-id', address='dummy-address'): """Factory for exchanges. """ if not isinstance(route, ExchangeRoute): network = route route = ExchangeRoute.from_network(participant, network) if not route: route = ExchangeRoute.insert(participant, network, address) assert route e_id = record_exchange(self.db, route, amount, fee, participant, 'pre', ref) record_exchange_result(self.db, e_id, status, error, participant) return e_id
def test_success_records_exchange(self): self.record_an_exchange({'amount': '10', 'fee': '0.50'}) expected = { "amount": D('10.00') , "fee": D('0.50') , "participant": "bob" , "recorder": "alice" , "note": "noted" , "route": ExchangeRoute.from_network(self.bob, 'paypal').id } SQL = "SELECT amount, fee, participant, recorder, note, route " \ "FROM exchanges" actual = self.db.one(SQL, back_as=dict) assert actual == expected
def set_paypal_email(username='', email='', api_key_fragment='', overwrite=False): """ Usage: [gratipay] $ env/bin/invoke set_paypal_email --username=username [email protected] [--api-key-fragment=12e4s678] [--overwrite] """ if not username or not email: print_help(set_paypal_email) sys.exit(1) if not os.environ.get('DATABASE_URL'): load_prod_envvars() if not api_key_fragment: first_eight = "unknown!" else: first_eight = api_key_fragment wireup.db(wireup.env()) participant = Participant.from_username(username) if not participant: print("No Gratipay participant found with username '" + username + "'") sys.exit(2) route = ExchangeRoute.from_network(participant, 'paypal') # PayPal caps the MassPay fee at $20 for users outside the U.S., and $1 for # users inside the U.S. Most Gratipay users using PayPal are outside the U.S. # so we set to $20 and I'll manually adjust to $1 when running MassPay and # noticing that something is off. FEE_CAP = 20 if route: print("PayPal email is already set to: " + route.address) if not overwrite: print("Not overwriting existing PayPal email.") sys.exit(3) if participant.api_key == None: assert first_eight == "None" else: assert participant.api_key[0:8] == first_eight print("Setting PayPal email for " + username + " to " + email) ExchangeRoute.insert(participant, 'paypal', email, fee_cap=FEE_CAP) print("All done.")
def test_success_records_exchange(self): self.record_an_exchange({'amount': '10', 'fee': '0.50'}) expected = { "amount": Decimal('10.00'), "fee": Decimal('0.50'), "participant": "bob", "recorder": "alice", "note": "noted", "route": ExchangeRoute.from_network(self.bob, 'paypal').id } SQL = "SELECT amount, fee, participant, recorder, note, route " \ "FROM exchanges" actual = self.db.one(SQL, back_as=dict) assert actual == expected
def create_card_hold(db, participant, amount): """Create a hold on the participant's credit card. Amount should be the nominal amount. We'll compute Gratipay's fee below this function and add it to amount to end up with charge_amount. """ typecheck(amount, Decimal) username = participant.username # Perform some last-minute checks. # ================================ if participant.is_suspicious is not False: raise NotWhitelisted # Participant not trusted. route = ExchangeRoute.from_network(participant, 'balanced-cc') if not route: return None, 'No credit card' # Go to Balanced. # =============== cents, amount_str, charge_amount, fee = _prep_hit(amount) msg = "Holding " + amount_str + " on Balanced for " + username + " ... " hold = None try: card = thing_from_href('cards', route.address) hold = card.hold( amount=cents , description=username , meta=dict(participant_id=participant.id, state='new') ) log(msg + "succeeded.") error = "" except Exception as e: error = repr_exception(e) log(msg + "failed: %s" % error) record_exchange(db, route, amount, fee, participant, 'failed', error) return hold, error
def record_an_exchange(self, data, make_participants=True): if make_participants: self.make_participants() data.setdefault('status', 'succeeded') data.setdefault('note', 'noted') if 'route_id' not in data: try: data['route_id'] = ExchangeRoute.insert(self.bob, 'paypal', '*****@*****.**').id except IntegrityError: data['route_id'] = ExchangeRoute.from_network(self.bob, 'paypal').id if data['status'] is None: del(data['status']) if data['route_id'] is None: del(data['route_id']) return self.client.PxST('/~bob/history/record-an-exchange', data, auth_as='alice')
def test_associate_bitcoin(self): addr = '17NdbrSGoUotzeGCcMMCqnFkEvLymoou9j' self.hit('roman', 'associate', 'bitcoin', addr) route = ExchangeRoute.from_network(self.roman, 'bitcoin') assert route.address == addr assert route.error == ''
def test_set_paypal_email(self): alice = self.make_participant('alice', api_key='abcdefgh') set_paypal_email(username='******', email='*****@*****.**', api_key_fragment=alice.api_key[0:8]) route = ExchangeRoute.from_network(alice, 'paypal') assert route.address == '*****@*****.**'
def ach_credit(db, participant, withhold, minimum_credit=MINIMUM_CREDIT): # Compute the amount to credit them. # ================================== # Leave money in Gratipay to cover their obligations next week (as these # currently stand). balance = participant.balance assert balance is not None, balance # sanity check amount = balance - withhold # Do some last-minute checks. # =========================== if amount <= 0: return # Participant not owed anything. if amount < minimum_credit: also_log = "" if withhold > 0: also_log = " ($%s balance - $%s in obligations)" also_log %= (balance, withhold) log("Minimum payout is $%s. %s is only due $%s%s." % (minimum_credit, participant.username, amount, also_log)) return # Participant owed too little. if not participant.is_whitelisted: raise NotWhitelisted # Participant not trusted. route = ExchangeRoute.from_network(participant, 'balanced-ba') if not route: return 'No bank account' # Do final calculations. # ====================== credit_amount, fee = skim_credit(amount) cents = credit_amount * 100 if withhold > 0: also_log = "$%s balance - $%s in obligations" also_log %= (balance, withhold) else: also_log = "$%s" % amount msg = "Crediting %s %d cents (%s - $%s fee = $%s) on Balanced ... " msg %= (participant.username, cents, also_log, fee, credit_amount) # Try to dance with Balanced. # =========================== e_id = record_exchange(db, route, -credit_amount, fee, participant, 'pre') meta = dict(exchange_id=e_id, participant_id=participant.id) try: ba = thing_from_href('bank_accounts', route.address) ba.credit(amount=cents, description=participant.username, meta=meta) record_exchange_result(db, e_id, 'pending', None, participant) log(msg + "succeeded.") error = "" except Exception as e: error = repr_exception(e) record_exchange_result(db, e_id, 'failed', error, participant) log(msg + "failed: %s" % error) return error
def test_associate_bitcoin_invalid(self): self.hit('roman', 'associate', 'bitcoin', '12345', expected=400) assert not ExchangeRoute.from_network(self.roman, 'bitcoin')
def create_card_hold(db, participant, amount): """Create a hold on the participant's credit card. Amount should be the nominal amount. We'll compute Gratipay's fee below this function and add it to amount to end up with charge_amount. """ typecheck(amount, Decimal) username = participant.username # Perform some last-minute checks. # ================================ if participant.is_suspicious is not False: raise NotWhitelisted # Participant not trusted. route = ExchangeRoute.from_network(participant, 'braintree-cc') if not route: return None, 'No credit card' # Go to Braintree. # ================ cents, amount_str, charge_amount, fee = _prep_hit(amount) amount = charge_amount - fee msg = "Holding " + amount_str + " on Braintree for " + username + " ... " hold = None error = "" try: result = braintree.Transaction.sale({ 'amount': str(cents/100.0), 'customer_id': route.participant.braintree_customer_id, 'payment_method_token': route.address, 'options': { 'submit_for_settlement': False }, 'custom_fields': {'participant_id': participant.id} }) if result.is_success and result.transaction.status == 'authorized': log(msg + "succeeded.") error = "" hold = result.transaction elif result.is_success: error = "Transaction status was %s" % result.transaction.status else: error = result.message if error == '': log(msg + "succeeded.") else: log(msg + "failed: %s" % error) record_exchange(db, route, amount, fee, participant, 'failed', error) except Exception as e: error = repr_exception(e) log(msg + "failed: %s" % error) record_exchange(db, route, amount, fee, participant, 'failed', error) return hold, error
def test_re_fails_if_negative_balance(self): alice = self.make_participant('alice', last_paypal_result='') ba = ExchangeRoute.from_network(alice, 'paypal') with pytest.raises(NegativeBalance): record_exchange(self.db, ba, D("-10.00"), D("0.41"), alice, 'pre')