def test_payment_providers_of_team(self): # 1. Test when the creator doesn't have any connected payment account. alice = self.make_participant('alice') data = {'name': 'Team1'} r = self.client.PxST('/about/teams', data, auth_as=alice) assert r.code == 302 team = Participant.from_username(data['name']) assert team.payment_providers == 0 # 2. Test when the creator has connected a PayPal account. self.add_payment_account(alice, 'paypal') data = {'name': 'Team2'} r = self.client.PxST('/about/teams', data, auth_as=alice) assert r.code == 302 team = Participant.from_username(data['name']) assert team.payment_providers == 2 # 3. Test after adding a member with a connected Stripe account. bob = self.make_participant('bob') self.add_payment_account(bob, 'stripe') team.add_member(bob) team = team.refetch() assert team.payment_providers == 3 # 4. Test after the creator leaves. team.set_take_for(alice, None, alice) team = team.refetch() assert team.payment_providers == 1
def test_dbtd_distributes_balance_as_final_gift(self): alice = self.make_participant('alice', balance=EUR('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, EUR('3.00')) alice.set_tip_to(carl, EUR('2.00')) alice.distribute_balances_to_donees() assert Participant.from_username('bob').balance == EUR('6.00') assert Participant.from_username('carl').balance == EUR('4.00') assert Participant.from_username('alice').balance == EUR('0.00')
def test_dbtd_favors_highest_tippee_in_rounding_errors(self): alice = self.make_participant('alice', balance=EUR('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, EUR('3.00')) alice.set_tip_to(carl, EUR('6.00')) alice.distribute_balances_to_donees() assert Participant.from_username('bob').balance == EUR('3.33') assert Participant.from_username('carl').balance == EUR('6.67') assert Participant.from_username('alice').balance == EUR('0.00')
def test_dbtd_with_zero_balance_is_a_noop(self): alice = self.make_participant('alice', balance=EUR('0.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, EUR('3.00')) alice.set_tip_to(carl, EUR('6.00')) alice.distribute_balances_to_donees() assert self.db.one("SELECT count(*) FROM tips") == 2 assert Participant.from_username('bob').balance == EUR('0.00') assert Participant.from_username('carl').balance == EUR('0.00') assert Participant.from_username('alice').balance == EUR('0.00')
def test_dbtd_skips_stopped_tips(self): alice = self.make_participant('alice', balance=EUR('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, EUR('1.00')) alice.stop_tip_to(bob) alice.set_tip_to(carl, EUR('2.00')) alice.distribute_balances_to_donees() assert Participant.from_username('bob').balance == EUR('0.00') assert Participant.from_username('carl').balance == EUR('10.00') assert Participant.from_username('alice').balance == EUR('0.00')
def test_dbafg_favors_highest_tippee_in_rounding_errors(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, D('3.00')) alice.set_tip_to(carl, D('6.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert Participant.from_username('bob').balance == D('3.33') assert Participant.from_username('carl').balance == D('6.67') assert Participant.from_username('alice').balance == D('0.00')
def test_can_post_to_close_page(self): alice = self.make_participant('alice', balance=7) bob = self.make_participant('bob') alice.set_tip_to(bob, D('10.00')) data = {'disburse_to': 'downstream'} response = self.client.PxST('/alice/settings/close', auth_as=alice, data=data) assert response.code == 302 assert response.headers['Location'] == '/alice/' assert Participant.from_username('alice').balance == 0 assert Participant.from_username('bob').balance == 7
def test_dbafg_distributes_balance_as_final_gift(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, D('3.00')) alice.set_tip_to(carl, D('2.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert Participant.from_username('bob').balance == D('6.00') assert Participant.from_username('carl').balance == D('4.00') assert Participant.from_username('alice').balance == D('0.00')
def test_dbafg_skips_zero_tips(self): alice = self.make_participant('alice', balance=D('10.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, D('0.00')) alice.set_tip_to(carl, D('2.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert self.db.one("SELECT count(*) FROM tips WHERE tippee=%s", (bob.id,)) == 1 assert Participant.from_username('bob').balance == D('0.00') assert Participant.from_username('carl').balance == D('10.00') assert Participant.from_username('alice').balance == D('0.00')
def test_dbafg_with_zero_balance_is_a_noop(self): alice = self.make_participant('alice', balance=D('0.00')) bob = self.make_participant('bob') carl = self.make_participant('carl') alice.set_tip_to(bob, D('3.00')) alice.set_tip_to(carl, D('6.00')) with self.db.get_cursor() as cursor: alice.distribute_balance_as_final_gift(cursor) assert self.db.one("SELECT count(*) FROM tips") == 2 assert Participant.from_username('bob').balance == D('0.00') assert Participant.from_username('carl').balance == D('0.00') assert Participant.from_username('alice').balance == D('0.00')
def test_payday_moves_money(self): self.janet.set_tip_to(self.homer, EUR('6.00')) # under $10! self.make_exchange('mango-cc', 10, 0, self.janet) Payday.start().run() janet = Participant.from_username('janet') homer = Participant.from_username('homer') assert homer.balance == EUR('6.00') assert janet.balance == EUR('4.00') assert self.transfer_mock.call_count
def test_sync_with_mangopay_transfers(self): self.make_exchange('mango-cc', 10, 0, self.janet) with mock.patch('liberapay.billing.exchanges.record_transfer_result') as rtr: rtr.side_effect = Foobar() with self.assertRaises(Foobar): transfer(self.db, self.janet.id, self.david.id, D('10.00'), 'tip') t = self.db.one("SELECT * FROM transfers") assert t.status == 'pre' sync_with_mangopay(self.db) t = self.db.one("SELECT * FROM transfers") assert t.status == 'succeeded' assert Participant.from_username('david').balance == 10 assert Participant.from_username('janet').balance == 0
def test_changes_to_others_take_can_increase_members_take(self): team, alice, bob = self.make_team_of_two() self.take_last_week(team, alice, '30.00') team.set_take_for(alice, D('25.00'), alice) self.take_last_week(team, bob, '50.00') team.set_take_for(bob, D('100.00'), bob) alice = Participant.from_username('alice') assert alice.receiving == alice.taking == 20 team.set_take_for(bob, D('75.00'), bob) alice = Participant.from_username('alice') assert alice.receiving == alice.taking == 25
def test_sync_with_mangopay_deletes_transfers_that_didnt_happen(self): self.make_exchange('mango-cc', 10, 0, self.janet) with mock.patch('liberapay.billing.exchanges.record_transfer_result') as rtr \ , mock.patch('liberapay.billing.mangoapi.transfers.Create') as Create: rtr.side_effect = Create.side_effect = Foobar with self.assertRaises(Foobar): transfer(self.db, self.janet.id, self.david.id, D('10.00'), 'tip') t = self.db.one("SELECT * FROM transfers") assert t.status == 'pre' sync_with_mangopay(self.db) transfers = self.db.all("SELECT * FROM transfers") assert not transfers assert Participant.from_username('david').balance == 0 assert Participant.from_username('janet').balance == 10
def test_payday_doesnt_move_money_to_a_suspicious_account(self): self.db.run(""" UPDATE participants SET is_suspicious = true WHERE username = '******' """) self.janet.set_tip_to(self.homer, '6.00') # under $10! Payday.start().run() janet = Participant.from_username('janet') homer = Participant.from_username('homer') assert janet.balance == D('0.00') assert homer.balance == D('0.00')
def test_3_sync_with_mangopay_handles_transfers_that_didnt_happen(self): self.make_exchange('mango-cc', 10, 0, self.janet) with mock.patch('liberapay.billing.transactions._record_transfer_result') as rtr, \ mock.patch('liberapay.billing.transactions.Transfer.save', autospec=True) as save: rtr.side_effect = save.side_effect = Foobar with self.assertRaises(Foobar): transfer(self.db, self.janet.id, self.david.id, EUR('10.00'), 'tip') t = self.db.one("SELECT * FROM transfers") assert t.status == 'pre' self.throw_transactions_back_in_time() sync_with_mangopay(self.db) t = self.db.one("SELECT * FROM transfers") assert t.status == 'failed' assert t.error == 'interrupted' assert Participant.from_username('david').balance == 0 assert Participant.from_username('janet').balance == 10
def fake_participant(db, kind=None, is_admin=False): """Create a fake User. """ username = faker.first_name() + fake_text_id(3) try: _fake_thing( db, "participants", username=username, password=None if kind == "group" else "x", email=username + "@example.org", is_admin=is_admin, balance=0, hide_giving=kind != "group" and (random.randrange(5) == 0), hide_receiving=kind != "group" and (random.randrange(5) == 0), is_suspicious=False, status="active", join_time=faker.date_time_this_year(), kind=kind or random.choice(("individual", "organization")), mangopay_user_id=username, mangopay_wallet_id="-1", ) except IntegrityError: return fake_participant(db, is_admin) # Call participant constructor to perform other DB initialization return Participant.from_username(username)
def test_verify_email_after_update(self): self.verify_and_change_email('*****@*****.**', '*****@*****.**') nonce = self.alice.get_email('*****@*****.**').nonce self.verify_email('*****@*****.**', nonce) expected = '*****@*****.**' actual = Participant.from_username('alice').email assert expected == actual
def test_verify_email(self): self.hit_email_spt('add-email', '*****@*****.**') nonce = self.alice.get_email('*****@*****.**').nonce self.verify_email('*****@*****.**', nonce) expected = '*****@*****.**' actual = Participant.from_username('alice').email assert expected == actual
def test_delete_card(self): self.hit('janet', 'delete', 'mango-cc', self.card_id) janet = Participant.from_username('janet') cards = ExchangeRoute.from_network(janet, 'mango-cc') assert not cards assert janet.mangopay_user_id
def test_receiving_includes_taking(self): alice = self.make_participant('alice') alice_card = self.upsert_route(alice, 'stripe-card') bob = self.make_participant('bob', taking=EUR('42.00')) alice.set_tip_to(bob, EUR('3.00')) self.make_payin_and_transfer(alice_card, bob, EUR('30.00')) assert Participant.from_username('bob').receiving == bob.receiving == EUR('45.00')
def test_delete_bank_account(self): self.hit('homer', 'delete', 'mango-ba', self.bank_account.Id) homer = Participant.from_username('homer') route = ExchangeRoute.from_address(homer, 'mango-ba', self.bank_account.Id) assert route.status == 'canceled' assert homer.mangopay_user_id
def fake_participant(db, kind=None, is_admin=False): """Create a fake User. """ username = faker.first_name() + fake_text_id(3) kind = kind or random.choice(('individual', 'organization')) is_a_person = kind in ('individual', 'organization') try: _fake_thing( db , "participants" , username=username , password=None if not is_a_person else 'x' , email=username+'@example.org' , is_admin=is_admin , balance=0 , hide_giving=is_a_person and (random.randrange(5) == 0) , hide_receiving=is_a_person and (random.randrange(5) == 0) , status='active' , join_time=faker.date_time_this_year() , kind=kind , mangopay_user_id=username , mangopay_wallet_id='-1' ) except IntegrityError: return fake_participant(db, is_admin) #Call participant constructor to perform other DB initialization return Participant.from_username(username)
def test_cpi_clears_personal_information(self, mailer): alice = self.make_participant( 'alice' , goal=100 , hide_giving=True , hide_receiving=True , avatar_url='img-url' , email='*****@*****.**' , session_token='deadbeef' , session_expires='2000-01-01' , giving=20 , pledging=30 , receiving=40 , npatrons=21 ) alice.upsert_statement('en', 'not forgetting to be awesome!') alice.add_email('*****@*****.**') with self.db.get_cursor() as cursor: alice.clear_personal_information(cursor) new_alice = Participant.from_username('alice') assert alice.get_statement(['en']) == (None, None) assert alice.goal == new_alice.goal == None assert alice.hide_giving == new_alice.hide_giving == True assert alice.hide_receiving == new_alice.hide_receiving == True assert alice.avatar_url == new_alice.avatar_url == None assert alice.email == new_alice.email assert alice.giving == new_alice.giving == 0 assert alice.pledging == new_alice.pledging == 0 assert alice.receiving == new_alice.receiving == 0 assert alice.npatrons == new_alice.npatrons == 0 assert alice.session_token == new_alice.session_token == None assert alice.session_expires.year == new_alice.session_expires.year == date.today().year assert not alice.get_emails()
def test_record_exchange_result_restores_balance_on_error(self): homer, ba = self.homer, self.homer_route self.make_exchange('mango-cc', 30, 0, homer) e_id = record_exchange(self.db, ba, D('-27.06'), D('0.81'), 0, homer, 'pre') assert homer.balance == D('02.13') record_exchange_result(self.db, e_id, 'failed', 'SOME ERROR', homer) homer = Participant.from_username('homer') assert homer.balance == D('30.00')
def test_record_exchange_result_doesnt_restore_balance_on_success(self): homer, ba = self.homer, self.homer_route self.make_exchange('mango-cc', 50, 0, homer) e_id = record_exchange(self.db, ba, D('-43.98'), D('1.60'), 0, homer, 'pre') assert homer.balance == D('4.42') record_exchange_result(self.db, e_id, 'succeeded', None, homer) homer = Participant.from_username('homer') assert homer.balance == D('4.42')
def test_record_exchange_result_updates_balance_for_positive_amounts(self): janet, cc = self.janet, self.janet_route self.make_exchange('mango-cc', 4, 0, janet) e_id = record_exchange(self.db, cc, D('31.59'), D('0.01'), 0, janet, 'pre') assert janet.balance == D('4.00') record_exchange_result(self.db, e_id, 'succeeded', None, janet) janet = Participant.from_username('janet') assert janet.balance == D('35.59')
def test_changes_to_team_receiving_affect_members_take(self): team, alice = self.make_team_of_one() self.take_last_week(team, alice, '40.00') team.set_take_for(alice, D('42.00'), alice) self.warbucks.set_tip_to(team, EUR('10.00')) # hard times alice = Participant.from_username('alice') assert alice.receiving == alice.taking == 10
def check(): for username in reversed(usernames[1:]): user = Participant.from_username(username) assert user.giving == EUR('1.00') assert user.receiving == EUR('1.00') assert user.npatrons == 1 funded_tips = self.db.all("SELECT id FROM tips WHERE is_funded ORDER BY id") assert len(funded_tips) == 6
def test_record_exchange_doesnt_update_balance_for_positive_amounts(self): record_exchange( self.db, self.janet_route, amount=D("0.59"), fee=D("0.41"), vat=D("0.00"), participant=self.janet, status='pre', ) janet = Participant.from_username('janet') assert self.janet.balance == janet.balance == D('0.00')
def test_delete_card(self): self.hit('janet', 'delete', 'mango-cc', self.card_id) janet = Participant.from_username('janet') assert janet.get_credit_card_error() == 'invalidated' assert janet.mangopay_user_id
def refund_payin(db, exchange, create_debts=False, refund_fee=False, dry_run=False): """Refund a specific payin. """ assert exchange.status == 'succeeded' and exchange.remote_id, exchange e_refund = db.one("SELECT e.* FROM exchanges e WHERE e.refund_ref = %s", (exchange.id, )) if e_refund and e_refund.status == 'succeeded': return 'already done', e_refund # Lock the bundles and try to swap them with db.get_cursor() as cursor: cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE") bundles = [ NS(d._asdict()) for d in cursor.all( """ UPDATE cash_bundles SET disputed = true WHERE origin = %s RETURNING * """, (exchange.id, )) ] bundles_sum = sum(b.amount for b in bundles) assert bundles_sum == exchange.amount original_owner = exchange.participant for b in bundles: if b.owner == original_owner: continue try_to_swap_bundle(cursor, b, original_owner) # Move the funds back to the original wallet LiberapayOrg = Participant.from_username('LiberapayOrg') assert LiberapayOrg return_payin_bundles_to_origin(db, exchange, LiberapayOrg, create_debts) # Add a debt for the fee if create_debts and refund_fee: create_debt(db, original_owner, LiberapayOrg.id, exchange.fee, exchange.id) # Compute and check the amount wallet = db.one("SELECT * FROM wallets WHERE remote_id = %s", (exchange.wallet_id, )) if e_refund and e_refund.status == 'pre': amount = -e_refund.amount else: amount = min(wallet.balance, exchange.amount) if amount <= 0: return ('not enough money: wallet balance = %s' % wallet.balance), None # Stop here if this is a dry run zero = exchange.fee.zero() fee, vat = (exchange.fee, exchange.vat) if refund_fee else (zero, zero) if dry_run: msg = ( '[dry run] full refund of payin #%s (liberapay id %s): amount = %s, fee = %s' % (exchange.remote_id, exchange.id, exchange.amount, exchange.fee) ) if amount + fee == exchange.amount + exchange.fee else ( '[dry run] partial refund of payin #%s (liberapay id %s): %s of %s, fee %s of %s' % (exchange.remote_id, exchange.id, amount, exchange.amount, fee, exchange.fee)) return msg, None # Record the refund attempt participant = Participant.from_id(exchange.participant) if not (e_refund and e_refund.status == 'pre'): with db.get_cursor() as cursor: cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE") bundles = [ NS(d._asdict()) for d in cursor.all( """ SELECT * FROM cash_bundles WHERE origin = %s AND wallet_id = %s AND disputed = true """, (exchange.id, exchange.wallet_id)) ] e_refund = cursor.one( """ INSERT INTO exchanges (participant, amount, fee, vat, route, status, refund_ref, wallet_id) VALUES (%s, %s, %s, %s, %s, 'pre', %s, %s) RETURNING * """, (participant.id, -amount, -fee, -vat, exchange.route, exchange.id, exchange.wallet_id)) cursor.run( """ INSERT INTO exchange_events (timestamp, exchange, status, wallet_delta) VALUES (%s, %s, 'pre', %s) """, (e_refund.timestamp, e_refund.id, e_refund.amount - e_refund.fee)) propagate_exchange(cursor, participant, e_refund, None, e_refund.amount, bundles=bundles) # Submit the refund m_refund = PayInRefund(payin_id=exchange.remote_id) m_refund.AuthorId = wallet.remote_owner_id m_refund.Tag = str(e_refund.id) m_refund.DebitedFunds = amount.int() m_refund.Fees = -fee.int() try: m_refund.save() except Exception as e: error = repr_exception(e) e_refund = record_exchange_result(db, e_refund.id, '', 'failed', error, participant) return 'exception', e_refund e_refund = record_exchange_result(db, e_refund.id, m_refund.Id, m_refund.Status.lower(), repr_error(m_refund), participant) return e_refund.status, e_refund
def test_verified_email_is_not_changed_after_update(self): self.add_and_verify_email('*****@*****.**') self.alice.add_email('*****@*****.**') expected = '*****@*****.**' actual = Participant.from_username('alice').email assert expected == actual
def test_changing_username_successfully(self): self.stub.change_username('user2') actual = Participant.from_username('user2') assert self.stub == actual
def test_changing_username_strips_spaces(self): self.stub.change_username(' aaa ') actual = Participant.from_username('aaa') assert self.stub == actual
def test_known_user_is_known(self): alice = self.make_participant('alice') alice2 = Participant.from_username('alice') assert alice == alice2
def test_username_is_case_insensitive(self): self.make_participant('AlIcE') actual = Participant.from_username('aLiCe').username assert actual == 'AlIcE'
def test_bad_username(self): p = Participant.from_username('deadbeef') assert not p
def test_rs_returns_openstreetmap_url_for_stub_from_openstreetmap(self): unclaimed = self.make_elsewhere('openstreetmap', '1', 'alice') stub = Participant.from_username(unclaimed.participant.username) actual = stub.resolve_stub() assert actual == "/on/openstreetmap/alice/"
def test_receiving_includes_taking_when_updated_from_set_tip_to(self): alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob', taking=Decimal('42.00')) alice.set_tip_to(bob, '3.00') assert Participant.from_username( 'bob').receiving == bob.receiving == Decimal('45.00')
def test_rs_returns_twitter_url_for_stub_from_twitter(self): unclaimed = self.make_elsewhere('twitter', '1234', 'alice') stub = Participant.from_username(unclaimed.participant.username) actual = stub.resolve_stub() assert actual == "/on/twitter/alice/"
def test_rs_returns_bitbucket_url_for_stub_from_bitbucket(self): unclaimed = self.make_elsewhere('bitbucket', '1234', 'alice') stub = Participant.from_username(unclaimed.participant.username) actual = stub.resolve_stub() assert actual == "/on/bitbucket/alice/"
def recover_lost_funds(db, exchange, lost_amount, repudiation_id): """Recover as much money as possible from a payin which has been reverted. """ original_owner = exchange.participant # Try (again) to swap the disputed bundles with db.get_cursor() as cursor: cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE") disputed_bundles = [ NS(d._asdict()) for d in cursor.all( """ SELECT * FROM cash_bundles WHERE origin = %s AND disputed = true """, (exchange.id, )) ] bundles_sum = sum(b.amount for b in disputed_bundles) assert bundles_sum == lost_amount - exchange.fee for b in disputed_bundles: if b.owner == original_owner: continue try_to_swap_bundle(cursor, b, original_owner) # Move the funds back to the original wallet currency = exchange.amount.currency chargebacks_account, credit_wallet = Participant.get_chargebacks_account( currency) LiberapayOrg = Participant.from_username('LiberapayOrg') assert LiberapayOrg grouped = group_by(disputed_bundles, lambda b: (b.owner, b.withdrawal)) for (owner, withdrawal), bundles in grouped.items(): assert owner != chargebacks_account.id if owner == original_owner: continue amount = sum(b.amount for b in bundles) if owner is None: bundles = None withdrawer = db.one( "SELECT participant FROM exchanges WHERE id = %s", (withdrawal, )) payer = LiberapayOrg.id create_debt(db, withdrawer, payer, amount, exchange.id) create_debt(db, original_owner, withdrawer, amount, exchange.id) else: payer = owner create_debt(db, original_owner, payer, amount, exchange.id) transfer(db, payer, original_owner, amount, 'chargeback', bundles=bundles) # Add a debt for the fee create_debt(db, original_owner, LiberapayOrg.id, exchange.fee, exchange.id) # Send the funds to the credit wallet # We have to do a SettlementTransfer instead of a normal Transfer. The amount # can't exceed the original payin amount, so we can't settle the fee debt. original_owner = Participant.from_id(original_owner) from_wallet = original_owner.get_current_wallet(currency).remote_id to_wallet = credit_wallet.remote_id t_id = prepare_transfer( db, original_owner.id, chargebacks_account.id, exchange.amount, 'chargeback', from_wallet, to_wallet, prefer_bundles_from=exchange.id, ) tr = SettlementTransfer() tr.AuthorId = original_owner.mangopay_user_id tr.CreditedUserId = chargebacks_account.mangopay_user_id tr.CreditedWalletId = to_wallet tr.DebitedFunds = exchange.amount.int() tr.DebitedWalletId = from_wallet tr.Fees = Money(0, currency) tr.RepudiationId = repudiation_id tr.Tag = str(t_id) return execute_transfer(db, t_id, tr)
def test_team_participant_does_show_up_on_explore_teams(self): alice = Participant.from_username('alice') self.make_participant('A-Team', kind='group').add_member(alice) assert 'A-Team' in self.client.GET("/explore/teams/").text