def test_paydays_json_gives_paydays(self): Payday.start() self.make_participant("alice") response = self.client.GET("/about/paydays.json") paydays = json.loads(response.body) assert paydays[0]['ntippers'] == 0
class TestPachinko(Harness): def setUp(self): Harness.setUp(self) self.payday = Payday(self.db) def test_get_participants_gets_participants(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20) a_team.add_member(self.make_participant('alice', claimed_time='now')) a_team.add_member(self.make_participant('bob', claimed_time='now')) ts_start = self.payday.start() actual = [p.username for p in self.payday.get_participants(ts_start)] expected = ['a_team', 'alice', 'bob'] assert actual == expected def test_pachinko_pachinkos(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20, pending=0) a_team.add_member(self.make_participant('alice', claimed_time='now', balance=0, pending=0)) a_team.add_member(self.make_participant('bob', claimed_time='now', balance=0, pending=0)) ts_start = self.payday.start() participants = self.payday.genparticipants(ts_start, ts_start) self.payday.pachinko(ts_start, participants)
def test_card_hold_error(self, Customer, fch): self.janet.set_tip_to(self.homer, 17) Customer.side_effect = Foobar fch.return_value = {} Payday.start().payin() payday = self.fetch_payday() assert payday['ncc_failing'] == 1
def payday(): # Wire things up. # =============== env = wireup.env() db = wireup.db(env) wireup.billing(env) wireup.nanswers(env) # Lazily import the billing module. # ================================= # This dodges a problem where db in billing is None if we import it from # gittip before calling wireup.billing. from gittip.billing.exchanges import sync_with_balanced from gittip.billing.payday import Payday try: with db.get_cursor() as cursor: sync_with_balanced(cursor) Payday.start().run() except KeyboardInterrupt: pass except: import aspen import traceback aspen.log(traceback.format_exc())
def test_payout_ach_error(self, ach_credit): self.make_participant('alice', claimed_time='now', is_suspicious=False, balance=20, balanced_customer_href='foo', last_ach_result='') ach_credit.return_value = 'some error' Payday.start().payout() payday = self.fetch_payday() assert payday['nach_failing'] == 1
def test_update_receiving_amounts_updates_receiving_amounts(self): A = self.make_participant('A') B = self.make_participant('B', claimed_time='now', last_bill_result='') B.set_tip_to(A, D('10.00'), update_tippee=False) assert Participant.from_username('A').receiving == 0 Payday.start().update_receiving_amounts() assert Participant.from_username('A').receiving == 10
def test_payin_doesnt_make_null_transfers(self): alice = self.make_participant('alice', claimed_time='now') alice.set_tip_to(self.homer, 1) alice.set_tip_to(self.homer, 0) a_team = self.make_participant('a_team', claimed_time='now', number='plural') a_team.add_member(alice) Payday.start().payin() transfers0 = self.db.all("SELECT * FROM transfers WHERE amount = 0") assert not transfers0
def test_payday_moves_money(self, fch): self.janet.set_tip_to(self.homer, '6.00') # under $10! fch.return_value = {} Payday.start().run() janet = Participant.from_username('janet') homer = Participant.from_username('homer') assert homer.balance == D('6.00') assert janet.balance == D('3.41')
def test_stats_description_accurate_outside_of_payday(self, mock_datetime): """Test stats page outside of the payday running""" a_monday = datetime(2012, 8, 6, 11, 00, 01) mock_datetime.utcnow.return_value = a_monday payday = Payday(self.postgres) payday.start() body = self.get_stats_page() assert "is ready for <b>this Thursday</b>" in body, body payday.end()
def test_payin_dumps_transfers_for_debugging(self, cch, fch): self.janet.set_tip_to(self.homer, 10) fake_hold = mock.MagicMock() fake_hold.amount = 1500 fch.return_value = {self.janet.id: fake_hold} cch.side_effect = Foobar open = mock.MagicMock() with mock.patch.dict(__builtins__, {'open': open}): with self.assertRaises(Foobar): Payday.start().payin() assert open.call_count == 1
def test_transfer_takes_doesnt_make_negative_transfers(self, fch): hold = balanced.CardHold(amount=1500, meta={'participant_id': self.janet.id}) hold.capture = lambda *a, **kw: None hold.save = lambda *a, **kw: None fch.return_value = {self.janet.id: hold} self.janet.update_number('plural') self.janet.set_tip_to(self.homer, 10) self.janet.add_member(self.david) Payday.start().payin() assert Participant.from_id(self.david.id).balance == 0 assert Participant.from_id(self.homer.id).balance == 10 assert Participant.from_id(self.janet.id).balance == 0
def test_payin_cancels_uncaptured_holds(self, log): self.janet.set_tip_to(self.homer, 42) alice = self.make_participant('alice', claimed_time='now', is_suspicious=False) self.make_exchange('bill', 50, 0, alice) alice.set_tip_to(self.janet, 50) Payday.start().payin() assert log.call_args_list[-3][0] == ("Captured 0 card holds.",) assert log.call_args_list[-2][0] == ("Canceled 1 card holds.",) assert Participant.from_id(alice.id).balance == 0 assert Participant.from_id(self.janet.id).balance == 8 assert Participant.from_id(self.homer.id).balance == 42
def test_stats_description_accurate_outside_of_payday(mock_datetime): """Test stats page outside of the payday running""" with testing.load() as context: a_monday = datetime(2012, 8, 6, 12, 00, 01) mock_datetime.utcnow.return_value = a_monday pd = Payday(context.db) pd.start() body = get_stats_page() assert "is ready for <b>this Thursday</b>" in body, body pd.end()
def test_iter_payday_events(self): Payday.start().run() team = self.make_participant('team', number='plural', claimed_time='now') alice = self.make_participant('alice', claimed_time='now') self.make_exchange('bill', 10000, 0, team) self.make_exchange('bill', 10000, 0, alice) self.make_exchange('bill', -5000, 0, alice) self.db.run(""" UPDATE transfers SET timestamp = "timestamp" - interval '1 month' """) bob = self.make_participant('bob', claimed_time='now') carl = self.make_participant('carl', claimed_time='now') team.add_member(bob) team.set_take_for(bob, Decimal('1.00'), team) alice.set_tip_to(bob, Decimal('5.00')) assert bob.balance == 0 for i in range(2): with patch.object(Payday, 'fetch_card_holds') as fch: fch.return_value = {} Payday.start().run() self.db.run(""" UPDATE paydays SET ts_start = ts_start - interval '1 week' , ts_end = ts_end - interval '1 week'; UPDATE transfers SET timestamp = "timestamp" - interval '1 week'; """) bob = Participant.from_id(bob.id) assert bob.balance == 12 Payday().start() events = list(iter_payday_events(self.db, bob)) assert len(events) == 8 assert events[0]['kind'] == 'day-open' assert events[0]['payday_number'] == 2 assert events[1]['balance'] == 12 assert events[-1]['kind'] == 'day-close' assert events[-1]['balance'] == '0.00' alice = Participant.from_id(alice.id) assert alice.balance == 4990 events = list(iter_payday_events(self.db, alice)) assert len(events) == 10 carl = Participant.from_id(carl.id) assert carl.balance == 0 events = list(iter_payday_events(self.db, carl)) assert len(events) == 0
def test_stats_description_accurate_outside_of_payday(self, utcnow): """Test stats page outside of the payday running""" a_monday = DateTime(2012, 8, 6, 11, 00, 01) utcnow.return_value = a_monday self.client.hydrate_website() payday = Payday(self.db) payday.start() body = self.get_stats_page() assert "is ready for <b>this Thursday</b>" in body, body payday.end()
def test_stats_description_accurate_outside_of_payday(self, mock_datetime): """Test stats page outside of the payday running""" self.clear_paydays() a_monday = datetime(2012, 8, 6, 12, 00, 01) mock_datetime.utcnow.return_value = a_monday db = wireup.db() wireup.billing() pd = Payday(db) pd.start() body = self.get_stats_page() self.assertTrue("is ready for <b>this Friday</b>" in body) pd.end()
def test_payday_doesnt_move_money_to_a_suspicious_account(self, fch): self.db.run(""" UPDATE participants SET is_suspicious = true WHERE username = '******' """) self.janet.set_tip_to(self.homer, '6.00') # under $10! fch.return_value = {} 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_start_prepare(self, log): self.clear_tables() self.make_participant('bob', balance=10, claimed_time=None) self.make_participant('carl', balance=10, claimed_time='now') payday = Payday.start() ts_start = payday.ts_start get_participants = lambda c: c.all("SELECT * FROM payday_participants") with self.db.get_cursor() as cursor: payday.prepare(cursor, ts_start) participants = get_participants(cursor) expected_logging_call_args = [ ('Starting a new payday.'), ('Payday started at {}.'.format(ts_start)), ('Prepared the DB.'), ] expected_logging_call_args.reverse() for args, _ in log.call_args_list: assert args[0] == expected_logging_call_args.pop() log.reset_mock() # run a second time, we should see it pick up the existing payday payday = Payday.start() second_ts_start = payday.ts_start with self.db.get_cursor() as cursor: payday.prepare(cursor, second_ts_start) second_participants = get_participants(cursor) assert ts_start == second_ts_start participants = list(participants) second_participants = list(second_participants) # carl is the only valid participant as he has a claimed time assert len(participants) == 1 assert participants == second_participants expected_logging_call_args = [ ('Picking up with an existing payday.'), ('Payday started at {}.'.format(second_ts_start)), ('Prepared the DB.'), ] expected_logging_call_args.reverse() for args, _ in log.call_args_list: assert args[0] == expected_logging_call_args.pop()
def test_stats_description_accurate_outside_of_payday(self): """Test stats page outside of the payday running""" # Hydrating a website requires a functioning datetime module. self.client.hydrate_website() a_monday = datetime.datetime(2012, 8, 6, 11, 00, 01) with patch.object(datetime, 'datetime') as mock_datetime: mock_datetime.utcnow.return_value = a_monday payday = Payday(self.db) payday.start() body = self.get_stats_page() assert "is ready for <b>this Thursday</b>" in body, body payday.end()
def test_stats_description_accurate_during_payday_run(self, mock_datetime): """Test that stats page takes running payday into account. This test was originally written to expose the fix required for https://github.com/gittip/www.gittip.com/issues/92. """ a_thursday = datetime(2012, 8, 9, 11, 00, 01) mock_datetime.utcnow.return_value = a_thursday wireup.billing() payday = Payday(self.postgres) payday.start() body = self.get_stats_page() assert "is changing hands <b>right now!</b>" in body, body payday.end()
def test_mark_charge_failed(self): payday = Payday.start() before = self.fetch_payday() with self.db.get_cursor() as cursor: payday.mark_charge_failed(cursor) after = self.fetch_payday() assert after['ncc_failing'] == before['ncc_failing'] + 1
def test_transfer_takes(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20) alice = self.make_participant('alice', claimed_time='now') a_team.add_member(alice) a_team.add_member(self.make_participant('bob', claimed_time='now')) a_team.set_take_for(alice, D('1.00'), alice) payday = Payday.start() # Test that payday ignores takes set after it started a_team.set_take_for(alice, D('2.00'), alice) # Run the transfer multiple times to make sure we ignore takes that # have already been processed for i in range(3): with self.db.get_cursor() as cursor: payday.prepare(cursor, payday.ts_start) payday.transfer_takes(cursor, payday.ts_start) payday.update_balances(cursor) participants = self.db.all("SELECT username, balance FROM participants") for p in participants: if p.username == 'a_team': assert p.balance == D('18.99') elif p.username == 'alice': assert p.balance == D('1.00') elif p.username == 'bob': assert p.balance == D('0.01') else: assert p.balance == 0
def test_stats_description_accurate_during_payday_run(mock_datetime): """Test that stats page takes running payday into account. This test was originally written to expose the fix required for https://github.com/whit537/www.gittip.com/issues/92. """ with testing.load() as context: a_thursday = datetime(2012, 8, 9, 12, 00, 01) mock_datetime.utcnow.return_value = a_thursday wireup.billing() pd = Payday(context.db) pd.start() body = get_stats_page() assert "is changing hands <b>right now!</b>" in body, body pd.end()
def test_stats_description_accurate_during_payday_run(self, mock_datetime): """Test that stats page takes running payday into account. This test was originally written to expose the fix required for https://github.com/whit537/www.gittip.com/issues/92. """ self.clear_paydays() a_friday = datetime(2012, 8, 10, 12, 00, 01) mock_datetime.utcnow.return_value = a_friday db = wireup.db() wireup.billing() pd = Payday(db) pd.start() body = self.get_stats_page() self.assertTrue("is changing hands <b>right now!</b>" in body) pd.end()
def test_stats_description_accurate_during_payday_run(self, utcnow): """Test that stats page takes running payday into account. This test was originally written to expose the fix required for https://github.com/gittip/www.gittip.com/issues/92. """ a_thursday = DateTime(2012, 8, 9, 11, 00, 01) utcnow.return_value = a_thursday self.client.hydrate_website() env = wireup.env() wireup.billing(env) payday = Payday(self.db) payday.start() body = self.get_stats_page() assert "is changing hands <b>right now!</b>" in body, body payday.end()
def test_stats_description_accurate_during_payday_run(self): """Test that stats page takes running payday into account. This test was originally written to expose the fix required for https://github.com/gittip/www.gittip.com/issues/92. """ # Hydrating a website requires a functioning datetime module. self.client.hydrate_website() a_thursday = datetime.datetime(2012, 8, 9, 11, 00, 01) with patch.object(datetime, 'datetime') as mock_datetime: mock_datetime.utcnow.return_value = a_thursday wireup.billing() payday = Payday(self.db) payday.start() body = self.get_stats_page() assert "is changing hands <b>right now!</b>" in body, body payday.end()
def test_payin_doesnt_process_tips_when_goal_is_negative(self): alice = self.make_participant('alice', claimed_time='now', balance=20) bob = self.make_participant('bob', claimed_time='now') alice.set_tip_to(bob, 13) self.db.run("UPDATE participants SET goal = -1 WHERE username='******'") payday = Payday.start() with self.db.get_cursor() as cursor: payday.prepare(cursor, payday.ts_start) payday.transfer_tips(cursor) payday.update_balances(cursor) assert Participant.from_id(alice.id).balance == 20 assert Participant.from_id(bob.id).balance == 0
class TestPachinko(Harness): def setUp(self): Harness.setUp(self) self.payday = Payday(self.db) def test_get_participants_gets_participants(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20) a_team.add_member(self.make_participant('alice', claimed_time='now')) a_team.add_member(self.make_participant('bob', claimed_time='now')) ts_start = self.payday.start() actual = [p.username for p in self.payday.get_participants(ts_start)] expected = ['a_team', 'alice', 'bob'] assert actual == expected def test_pachinko_pachinkos(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20, pending=0) a_team.add_member( self.make_participant('alice', claimed_time='now', balance=0, pending=0)) a_team.add_member( self.make_participant('bob', claimed_time='now', balance=0, pending=0)) ts_start = self.payday.start() participants = self.payday.genparticipants(ts_start, ts_start) self.payday.pachinko(ts_start, participants)
def test_payin_cant_make_balances_more_negative(self): self.db.run(""" UPDATE participants SET balance = -10 WHERE username='******' """) payday = Payday.start() with self.db.get_cursor() as cursor: payday.prepare(cursor, payday.ts_start) cursor.run(""" UPDATE payday_participants SET new_balance = -50 WHERE username IN ('janet', 'homer') """) with self.assertRaises(NegativeBalance): payday.update_balances(cursor)
def test_payday_moves_money_with_balanced(self, fch): self.janet.set_tip_to(self.homer, '15.00') fch.return_value = {} 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') janet_customer = balanced.Customer.fetch(janet.balanced_customer_href) homer_customer = balanced.Customer.fetch(homer.balanced_customer_href) created_at = balanced.Transaction.f.created_at credit = homer_customer.credits.sort(created_at.desc()).first() assert credit.amount == 1500 assert credit.description == 'homer' debit = janet_customer.debits.sort(created_at.desc()).first() assert debit.amount == 1576 # base amount + fee assert debit.description == 'janet'
def test_transfer_tips(self): alice = self.make_participant('alice', claimed_time='now', balance=1, last_bill_result='') alice.set_tip_to(self.janet, D('0.51')) alice.set_tip_to(self.homer, D('0.50')) payday = Payday.start() with self.db.get_cursor() as cursor: payday.prepare(cursor, payday.ts_start) payday.transfer_tips(cursor) payday.update_balances(cursor) alice = Participant.from_id(alice.id) assert Participant.from_id(alice.id).balance == D('0.49') assert Participant.from_id(self.janet.id).balance == D('0.51') assert Participant.from_id(self.homer.id).balance == 0
class TestPachinko(Harness): def setUp(self): Harness.setUp(self) self.payday = Payday(self.db) def test_get_participants_gets_participants(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20) a_team.add_member(self.make_participant('alice', claimed_time='now')) a_team.add_member(self.make_participant('bob', claimed_time='now')) ts_start = self.payday.start() actual = [p.username for p in self.payday.get_participants(ts_start)] expected = ['a_team', 'alice', 'bob'] assert actual == expected def test_pachinko_pachinkos(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20, \ pending=0) a_team.add_member( self.make_participant('alice', claimed_time='now', balance=0, pending=0)) a_team.add_member( self.make_participant('bob', claimed_time='now', balance=0, pending=0)) ts_start = self.payday.start() participants = self.payday.genparticipants(ts_start, LOOP_PACHINKO) self.payday.pachinko(ts_start, participants) assert Participant.from_username('alice').pending == D('0.01') assert Participant.from_username('bob').pending == D('0.01') def test_pachinko_sees_current_take(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20, \ pending=0) alice = self.make_participant('alice', claimed_time='now', balance=0, pending=0) a_team.add_member(alice) a_team.set_take_for(alice, D('1.00'), alice) ts_start = self.payday.start() participants = self.payday.genparticipants(ts_start, LOOP_PACHINKO) self.payday.pachinko(ts_start, participants) assert Participant.from_username('alice').pending == D('1.00') def test_pachinko_ignores_take_set_after_payday_starts(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20, \ pending=0) alice = self.make_participant('alice', claimed_time='now', balance=0, pending=0) a_team.add_member(alice) a_team.set_take_for(alice, D('0.33'), alice) ts_start = self.payday.start() a_team.set_take_for(alice, D('1.00'), alice) participants = self.payday.genparticipants(ts_start, LOOP_PACHINKO) self.payday.pachinko(ts_start, participants) assert Participant.from_username('alice').pending == D('0.33') def test_pachinko_ignores_take_thats_already_been_processed(self): a_team = self.make_participant('a_team', claimed_time='now', number='plural', balance=20, \ pending=0) alice = self.make_participant('alice', claimed_time='now', balance=0, pending=0) a_team.add_member(alice) a_team.set_take_for(alice, D('0.33'), alice) ts_start = self.payday.start() a_team.set_take_for(alice, D('1.00'), alice) for i in range(4): participants = self.payday.genparticipants(ts_start, LOOP_PACHINKO) self.payday.pachinko(ts_start, participants) assert Participant.from_username('alice').pending == D('0.33')
class TestBillingCharges(Harness): BALANCED_ACCOUNT_URI = u'/v1/marketplaces/M123/accounts/A123' BALANCED_TOKEN = u'/v1/marketplaces/M123/accounts/A123/cards/C123' STRIPE_CUSTOMER_ID = u'cus_deadbeef' def setUp(self): super(TestBillingCharges, self).setUp() self.payday = Payday(self.postgres) self.alice = self.make_participant('alice') def test_mark_missing_funding(self): self.payday.start() before = PaydayModel.query.first().attrs_dict() missing_count = before['ncc_missing'] self.payday.mark_missing_funding() after = PaydayModel.query.first().attrs_dict() self.assertEqual(after['ncc_missing'], missing_count + 1) def test_mark_charge_failed(self): self.payday.start() before = PaydayModel.query.first().attrs_dict() fail_count = before['ncc_failing'] with self.postgres.get_connection() as conn: cur = conn.cursor() self.payday.mark_charge_failed(cur) conn.commit() after = PaydayModel.query.first().attrs_dict() self.assertEqual(after['ncc_failing'], fail_count + 1) def test_mark_charge_success(self): self.payday.start() charge_amount, fee = 4, 2 with self.postgres.get_connection() as conn: cur = conn.cursor() self.payday.mark_charge_success(cur, charge_amount, fee) conn.commit() # verify paydays actual = PaydayModel.query.first().attrs_dict() self.assertEqual(actual['ncharges'], 1) @mock.patch('stripe.Charge') def test_charge_on_stripe(self, ba): amount_to_charge = Decimal('10.00') # $10.00 USD expected_fee = (amount_to_charge + FEE_CHARGE[0]) * FEE_CHARGE[1] expected_fee = (amount_to_charge - expected_fee.quantize( FEE_CHARGE[0], rounding=ROUND_UP)) * -1 charge_amount, fee, msg = self.payday.charge_on_stripe( self.alice.id, self.STRIPE_CUSTOMER_ID, amount_to_charge) assert_equals(charge_amount, amount_to_charge + fee) assert_equals(fee, expected_fee) self.assertTrue(ba.find.called_with(self.STRIPE_CUSTOMER_ID)) customer = ba.find.return_value self.assertTrue( customer.debit.called_with(int(charge_amount * 100), self.alice.id)) @mock.patch('balanced.Account') def test_charge_on_balanced(self, ba): amount_to_charge = Decimal('10.00') # $10.00 USD expected_fee = (amount_to_charge + FEE_CHARGE[0]) * FEE_CHARGE[1] expected_fee = (amount_to_charge - expected_fee.quantize( FEE_CHARGE[0], rounding=ROUND_UP)) * -1 charge_amount, fee, msg = self.payday.charge_on_balanced( self.alice.id, self.BALANCED_ACCOUNT_URI, amount_to_charge) self.assertEqual(charge_amount, amount_to_charge + fee) self.assertEqual(fee, expected_fee) self.assertTrue(ba.find.called_with(self.BALANCED_ACCOUNT_URI)) customer = ba.find.return_value self.assertTrue( customer.debit.called_with(int(charge_amount * 100), self.alice.id)) @mock.patch('balanced.Account') def test_charge_on_balanced_small_amount(self, ba): amount_to_charge = Decimal('0.06') # $0.06 USD expected_fee = Decimal('0.68') expected_amount = Decimal('10.00') charge_amount, fee, msg = \ self.payday.charge_on_balanced(self.alice.id, self.BALANCED_ACCOUNT_URI, amount_to_charge) assert_equals(charge_amount, expected_amount) assert_equals(fee, expected_fee) customer = ba.find.return_value self.assertTrue( customer.debit.called_with(int(charge_amount * 100), self.alice.id)) @mock.patch('balanced.Account') def test_charge_on_balanced_failure(self, ba): amount_to_charge = Decimal('0.06') # $0.06 USD error_message = 'Woah, crazy' ba.find.side_effect = balanced.exc.HTTPError(error_message) charge_amount, fee, msg = self.payday.charge_on_balanced( self.alice.id, self.BALANCED_ACCOUNT_URI, amount_to_charge) assert_equals(msg, error_message)
class TestBillingTransfer(Harness): def setUp(self): super(Harness, self).setUp() self.payday = Payday(self.postgres) self.payday.start() self.tipper = self.make_participant('lgtest') self.balanced_account_uri = '/v1/marketplaces/M123/accounts/A123' def test_transfer(self): amount = Decimal('1.00') sender = self.make_participant('test_transfer_sender', pending=0, balance=1) recipient = self.make_participant('test_transfer_recipient', pending=0, balance=1) result = self.payday.transfer(sender.username, recipient.username, amount) assert_equals(result, True) # no balance remaining for a second transfer result = self.payday.transfer(sender.username, recipient.username, amount) assert_equals(result, False) def test_debit_participant(self): amount = Decimal('1.00') subject = self.make_participant('test_debit_participant', pending=0, balance=1) initial_amount = subject.balance with self.postgres.get_connection() as connection: cursor = connection.cursor() self.payday.debit_participant(cursor, subject.username, amount) connection.commit() self.session.refresh(subject) expected = initial_amount - amount actual = subject.balance assert_equals(actual, expected) # this will fail because not enough balance with self.postgres.get_connection() as conn: cur = conn.cursor() with self.assertRaises(IntegrityError): self.payday.debit_participant(cur, subject.username, amount) conn.commit() def test_skim_credit(self): actual = skim_credit(Decimal('10.00')) assert actual == (Decimal('10.00'), Decimal('0.00')), actual def test_credit_participant(self): amount = Decimal('1.00') subject = self.make_participant('test_credit_participant', pending=0, balance=1) initial_amount = subject.pending with self.postgres.get_connection() as conn: cur = conn.cursor() self.payday.credit_participant(cur, subject.username, amount) conn.commit() self.session.refresh(subject) expected = initial_amount + amount actual = subject.pending assert_equals(actual, expected) def test_record_transfer(self): from gittip.models import Transfer amount = Decimal('1.00') subjects = ['jim', 'kate', 'bob'] for subject in subjects: self.make_participant(subject, balance=1, pending=0) with self.postgres.get_connection() as conn: cur = conn.cursor() # Tip 'jim' twice for recipient in ['jim'] + subjects: self.payday.record_transfer(cur, self.tipper.username, recipient, amount) conn.commit() for subject in subjects: # 'jim' is tipped twice expected = amount * 2 if subject == 'jim' else amount transfers = Transfer.query.filter_by(tippee=subject).all() actual = sum(tip.amount for tip in transfers) assert_equals(actual, expected) def test_record_transfer_invalid_participant(self): amount = Decimal('1.00') with self.postgres.get_connection() as conn: cur = conn.cursor() with assert_raises(IntegrityError): self.payday.record_transfer(cur, 'idontexist', 'nori', amount) conn.commit() def test_mark_transfer(self): amount = Decimal('1.00') # Forces a load with current state in dict before_transfer = PaydayModel.query.first().attrs_dict() with self.postgres.get_connection() as conn: cur = conn.cursor() self.payday.mark_transfer(cur, amount) conn.commit() # Forces a load with current state in dict after_transfer = PaydayModel.query.first().attrs_dict() expected = before_transfer['ntransfers'] + 1 actual = after_transfer['ntransfers'] assert_equals(actual, expected) expected = before_transfer['transfer_volume'] + amount actual = after_transfer['transfer_volume'] assert_equals(actual, expected) def test_record_credit_updates_balance(self): alice = self.make_participant("alice") self.payday.record_credit(amount=Decimal("-1.00"), fee=Decimal("0.41"), error="", username="******") assert_equals(alice.balance, Decimal("0.59")) def test_record_credit_doesnt_update_balance_if_error(self): alice = self.make_participant("alice") self.payday.record_credit(amount=Decimal("-1.00"), fee=Decimal("0.41"), error="SOME ERROR", username="******") assert_equals(alice.balance, Decimal("0.00"))
class TestBillingCharges(TestPaydayBase): BALANCED_ACCOUNT_URI = u'/v1/marketplaces/M123/accounts/A123' BALANCED_TOKEN = u'/v1/marketplaces/M123/accounts/A123/cards/C123' STRIPE_CUSTOMER_ID = u'cus_deadbeef' def setUp(self): super(TestBillingCharges, self).setUp() self.payday = Payday(self.db) def test_mark_missing_funding(self): self.payday.start() before = self.fetch_payday() missing_count = before['ncc_missing'] self.payday.mark_missing_funding() after = self.fetch_payday() self.assertEqual(after['ncc_missing'], missing_count + 1) def test_mark_charge_failed(self): self.payday.start() before = self.fetch_payday() fail_count = before['ncc_failing'] with self.db.get_cursor() as cursor: self.payday.mark_charge_failed(cursor) after = self.fetch_payday() self.assertEqual(after['ncc_failing'], fail_count + 1) def test_mark_charge_success(self): self.payday.start() charge_amount, fee = 4, 2 with self.db.get_cursor() as cursor: self.payday.mark_charge_success(cursor, charge_amount, fee) # verify paydays actual = self.fetch_payday() self.assertEqual(actual['ncharges'], 1) @mock.patch('stripe.Charge') def test_charge_on_stripe(self, ba): amount_to_charge = Decimal('10.00') # $10.00 USD expected_fee = Decimal('0.61') charge_amount, fee, msg = self.payday.charge_on_stripe( self.alice.username, self.STRIPE_CUSTOMER_ID, amount_to_charge) assert_equals(charge_amount, amount_to_charge + fee) assert_equals(fee, expected_fee) self.assertTrue(ba.find.called_with(self.STRIPE_CUSTOMER_ID)) customer = ba.find.return_value self.assertTrue( customer.debit.called_with(int(charge_amount * 100), self.alice.username)) @mock.patch('balanced.Account') def test_charge_on_balanced(self, ba): amount_to_charge = Decimal('10.00') # $10.00 USD expected_fee = Decimal('0.61') charge_amount, fee, msg = self.payday.charge_on_balanced( self.alice.username, self.BALANCED_ACCOUNT_URI, amount_to_charge) self.assertEqual(charge_amount, amount_to_charge + fee) self.assertEqual(fee, expected_fee) self.assertTrue(ba.find.called_with(self.BALANCED_ACCOUNT_URI)) customer = ba.find.return_value self.assertTrue( customer.debit.called_with(int(charge_amount * 100), self.alice.username)) @mock.patch('balanced.Account') def test_charge_on_balanced_small_amount(self, ba): amount_to_charge = Decimal('0.06') # $0.06 USD expected_fee = Decimal('0.59') expected_amount = Decimal('10.00') charge_amount, fee, msg = \ self.payday.charge_on_balanced(self.alice.username, self.BALANCED_ACCOUNT_URI, amount_to_charge) assert_equals(charge_amount, expected_amount) assert_equals(fee, expected_fee) customer = ba.find.return_value self.assertTrue( customer.debit.called_with(int(charge_amount * 100), self.alice.username)) @mock.patch('balanced.Account') def test_charge_on_balanced_failure(self, ba): amount_to_charge = Decimal('0.06') # $0.06 USD error_message = 'Woah, crazy' ba.find.side_effect = balanced.exc.HTTPError(error_message) charge_amount, fee, msg = self.payday.charge_on_balanced( self.alice.username, self.BALANCED_ACCOUNT_URI, amount_to_charge) assert_equals(msg, error_message)
class TestBillingTransfer(TestPaydayBase): def setUp(self): super(TestPaydayBase, self).setUp() self.payday = Payday(self.db) self.payday.start() self.tipper = self.make_participant('lgtest') self.balanced_account_uri = '/v1/marketplaces/M123/accounts/A123' def test_transfer(self): amount = Decimal('1.00') sender = self.make_participant('test_transfer_sender', pending=0, balance=1) recipient = self.make_participant('test_transfer_recipient', pending=0, balance=1) result = self.payday.transfer(sender.username, recipient.username, amount) assert_equals(result, True) # no balance remaining for a second transfer result = self.payday.transfer(sender.username, recipient.username, amount) assert_equals(result, False) def test_debit_participant(self): amount = Decimal('1.00') subject = self.make_participant('test_debit_participant', pending=0, balance=1) initial_amount = subject.balance with self.db.get_cursor() as cursor: self.payday.debit_participant(cursor, subject.username, amount) subject = Participant.from_username('test_debit_participant') expected = initial_amount - amount actual = subject.balance assert_equals(actual, expected) # this will fail because not enough balance with self.db.get_cursor() as cursor: with self.assertRaises(IntegrityError): self.payday.debit_participant(cursor, subject.username, amount) def test_skim_credit(self): actual = skim_credit(Decimal('10.00')) assert actual == (Decimal('10.00'), Decimal('0.00')), actual def test_credit_participant(self): amount = Decimal('1.00') subject = self.make_participant('test_credit_participant', pending=0, balance=1) initial_amount = subject.pending with self.db.get_cursor() as cursor: self.payday.credit_participant(cursor, subject.username, amount) subject = Participant.from_username( 'test_credit_participant') # reload expected = initial_amount + amount actual = subject.pending assert_equals(actual, expected) def test_record_transfer(self): amount = Decimal('1.00') subjects = ['jim', 'kate', 'bob'] for subject in subjects: self.make_participant(subject, balance=1, pending=0) with self.db.get_cursor() as cursor: # Tip 'jim' twice for recipient in ['jim'] + subjects: self.payday.record_transfer(cursor, self.tipper.username, recipient, amount) for subject in subjects: # 'jim' is tipped twice expected = amount * 2 if subject == 'jim' else amount actual = self.db.one( "SELECT sum(amount) FROM transfers " "WHERE tippee=%s", (subject, )) assert_equals(actual, expected) def test_record_transfer_invalid_participant(self): amount = Decimal('1.00') with self.db.get_cursor() as cursor: with assert_raises(IntegrityError): self.payday.record_transfer(cursor, 'idontexist', 'nori', amount) def test_mark_transfer(self): amount = Decimal('1.00') # Forces a load with current state in dict before_transfer = self.fetch_payday() with self.db.get_cursor() as cursor: self.payday.mark_transfer(cursor, amount) # Forces a load with current state in dict after_transfer = self.fetch_payday() expected = before_transfer['ntransfers'] + 1 actual = after_transfer['ntransfers'] assert_equals(actual, expected) expected = before_transfer['transfer_volume'] + amount actual = after_transfer['transfer_volume'] assert_equals(actual, expected) def test_record_credit_updates_balance(self): self.payday.record_credit(amount=Decimal("-1.00"), fee=Decimal("0.41"), error="", username="******") alice = Participant.from_username('alice') assert_equals(alice.balance, Decimal("0.59")) def test_record_credit_doesnt_update_balance_if_error(self): self.payday.record_credit(amount=Decimal("-1.00"), fee=Decimal("0.41"), error="SOME ERROR", username="******") alice = Participant.from_username('alice') assert_equals(alice.balance, Decimal("0.00"))
class TestBillingPayday(TestPaydayBase): BALANCED_ACCOUNT_URI = '/v1/marketplaces/M123/accounts/A123' def setUp(self): super(TestBillingPayday, self).setUp() self.payday = Payday(self.db) @mock.patch('gittip.models.participant.Participant.get_tips_and_total') def test_charge_and_or_transfer_no_tips(self, get_tips_and_total): self.db.run( """ UPDATE participants SET balance=1 , balanced_account_uri=%s , is_suspicious=False WHERE username='******' """, (self.BALANCED_ACCOUNT_URI, )) amount = Decimal('1.00') ts_start = self.payday.start() tips, total = [], amount initial_payday = self.fetch_payday() self.payday.charge_and_or_transfer(ts_start, self.alice, tips, total) resulting_payday = self.fetch_payday() assert_equals(initial_payday['ntippers'], resulting_payday['ntippers']) assert_equals(initial_payday['ntips'], resulting_payday['ntips']) assert_equals(initial_payday['nparticipants'] + 1, resulting_payday['nparticipants']) @mock.patch('gittip.models.participant.Participant.get_tips_and_total') @mock.patch('gittip.billing.payday.Payday.tip') def test_charge_and_or_transfer(self, tip, get_tips_and_total): self.db.run( """ UPDATE participants SET balance=1 , balanced_account_uri=%s , is_suspicious=False WHERE username='******' """, (self.BALANCED_ACCOUNT_URI, )) ts_start = self.payday.start() now = datetime.utcnow() amount = Decimal('1.00') like_a_tip = { 'amount': amount, 'tippee': 'mjallday', 'ctime': now, 'claimed_time': now } # success, success, claimed, failure tips = [like_a_tip, like_a_tip, like_a_tip, like_a_tip] total = amount ts_start = datetime.utcnow() return_values = [1, 1, 0, -1] return_values.reverse() def tip_return_values(*_): return return_values.pop() tip.side_effect = tip_return_values initial_payday = self.fetch_payday() self.payday.charge_and_or_transfer(ts_start, self.alice, tips, total) resulting_payday = self.fetch_payday() assert_equals(initial_payday['ntippers'] + 1, resulting_payday['ntippers']) assert_equals(initial_payday['ntips'] + 2, resulting_payday['ntips']) assert_equals(initial_payday['nparticipants'] + 1, resulting_payday['nparticipants']) @mock.patch('gittip.models.participant.Participant.get_tips_and_total') @mock.patch('gittip.billing.payday.Payday.charge') def test_charge_and_or_transfer_short(self, charge, get_tips_and_total): self.db.run( """ UPDATE participants SET balance=1 , balanced_account_uri=%s , is_suspicious=False WHERE username='******' """, (self.BALANCED_ACCOUNT_URI, )) now = datetime.utcnow() amount = Decimal('1.00') like_a_tip = { 'amount': amount, 'tippee': 'mjallday', 'ctime': now, 'claimed_time': now } # success, success, claimed, failure tips = [like_a_tip, like_a_tip, like_a_tip, like_a_tip] get_tips_and_total.return_value = tips, amount ts_start = datetime.utcnow() # In real-life we wouldn't be able to catch an error as the charge # method will swallow any errors and return false. We don't handle this # return value within charge_and_or_transfer but instead continue on # trying to use the remaining credit in the user's account to payout as # many tips as possible. # # Here we're hacking the system and throwing the exception so execution # stops since we're only testing this part of the method. That smells # like we need to refactor. charge.side_effect = Exception() with self.assertRaises(Exception): billing.charge_and_or_transfer(ts_start, self.alice) self.assertTrue( charge.called_with(self.alice.username, self.BALANCED_ACCOUNT_URI, amount)) @mock.patch('gittip.billing.payday.Payday.transfer') @mock.patch('gittip.billing.payday.log') def test_tip(self, log, transfer): self.db.run( """ UPDATE participants SET balance=1 , balanced_account_uri=%s , is_suspicious=False WHERE username='******' """, (self.BALANCED_ACCOUNT_URI, )) amount = Decimal('1.00') invalid_amount = Decimal('0.00') tip = { 'amount': amount, 'tippee': self.alice.username, 'claimed_time': utcnow() } ts_start = utcnow() result = self.payday.tip(self.alice, tip, ts_start) assert_equals(result, 1) result = transfer.called_with(self.alice.username, tip['tippee'], tip['amount']) self.assertTrue(result) self.assertTrue(log.called_with('SUCCESS: $1 from mjallday to alice.')) # XXX: Should these tests be broken down to a separate class with the # common setup factored in to a setUp method. # XXX: We should have constants to compare the values to # invalid amount tip['amount'] = invalid_amount result = self.payday.tip(self.alice, tip, ts_start) assert_equals(result, 0) tip['amount'] = amount # XXX: We should have constants to compare the values to # not claimed tip['claimed_time'] = None result = self.payday.tip(self.alice, tip, ts_start) assert_equals(result, 0) # XXX: We should have constants to compare the values to # claimed after payday tip['claimed_time'] = utcnow() result = self.payday.tip(self.alice, tip, ts_start) assert_equals(result, 0) ts_start = utcnow() # XXX: We should have constants to compare the values to # transfer failed transfer.return_value = False result = self.payday.tip(self.alice, tip, ts_start) assert_equals(result, -1) @mock.patch('gittip.billing.payday.log') def test_start_zero_out_and_get_participants(self, log): self.make_participant('bob', balance=10, claimed_time=None, pending=1, balanced_account_uri=self.BALANCED_ACCOUNT_URI) self.make_participant('carl', balance=10, claimed_time=utcnow(), pending=1, balanced_account_uri=self.BALANCED_ACCOUNT_URI) self.db.run( """ UPDATE participants SET balance=0 , claimed_time=null , pending=null , balanced_account_uri=%s WHERE username='******' """, (self.BALANCED_ACCOUNT_URI, )) ts_start = self.payday.start() self.payday.zero_out_pending(ts_start) participants = self.payday.get_participants(ts_start) expected_logging_call_args = [ ('Starting a new payday.'), ('Payday started at {}.'.format(ts_start)), ('Zeroed out the pending column.'), ('Fetched participants.'), ] expected_logging_call_args.reverse() for args, _ in log.call_args_list: assert_equals(args[0], expected_logging_call_args.pop()) log.reset_mock() # run a second time, we should see it pick up the existing payday second_ts_start = self.payday.start() self.payday.zero_out_pending(second_ts_start) second_participants = self.payday.get_participants(second_ts_start) self.assertEqual(ts_start, second_ts_start) participants = list(participants) second_participants = list(second_participants) # carl is the only valid participant as he has a claimed time assert_equals(len(participants), 1) assert_equals(participants, second_participants) expected_logging_call_args = [ ('Picking up with an existing payday.'), ('Payday started at {}.'.format(second_ts_start)), ('Zeroed out the pending column.'), ('Fetched participants.') ] expected_logging_call_args.reverse() for args, _ in log.call_args_list: assert_equals(args[0], expected_logging_call_args.pop()) @mock.patch('gittip.billing.payday.log') def test_end(self, log): self.payday.start() self.payday.end() self.assertTrue(log.called_with('Finished payday.')) # finishing the payday will set the ts_end date on this payday record # to now, so this will not return any result result = self.db.one("SELECT count(*) FROM paydays " "WHERE ts_end > '1970-01-01'") assert_equals(result, 1) @mock.patch('gittip.billing.payday.log') @mock.patch('gittip.billing.payday.Payday.start') @mock.patch('gittip.billing.payday.Payday.payin') @mock.patch('gittip.billing.payday.Payday.end') def test_payday(self, end, payin, init, log): ts_start = utcnow() init.return_value = (ts_start, ) greeting = 'Greetings, program! It\'s PAYDAY!!!!' self.payday.run() self.assertTrue(log.called_with(greeting)) self.assertTrue(init.call_count) self.assertTrue(payin.called_with(init.return_value)) self.assertTrue(end.call_count)
class TestPaydayCharge(TestPaydayBase): STRIPE_CUSTOMER_ID = 'cus_deadbeef' def setUp(self): super(TestBillingBase, self).setUp() self.payday = Payday(self.db) def get_numbers(self): """Return a list of 9 ints: nachs nach_failing ncc_failing ncc_missing ncharges npachinko nparticipants ntippers ntips ntransfers """ payday = self.fetch_payday() keys = [key for key in sorted(payday) if key.startswith('n')] return [payday[key] for key in keys] def test_charge_without_cc_details_returns_None(self): alice = self.make_participant('alice') self.payday.start() actual = self.payday.charge(alice, Decimal('1.00')) assert actual is None, actual def test_charge_without_cc_marked_as_failure(self): alice = self.make_participant('alice') self.payday.start() self.payday.charge(alice, Decimal('1.00')) actual = self.get_numbers() assert_equals(actual, [0, 0, 0, 1, 0, 0, 0, 0, 0, 0]) @mock.patch('gittip.billing.payday.Payday.charge_on_balanced') def test_charge_failure_returns_None(self, cob): cob.return_value = (Decimal('10.00'), Decimal('0.68'), 'FAILED') bob = self.make_participant( 'bob', last_bill_result="failure", balanced_account_uri=self.balanced_account_uri, stripe_customer_id=self.STRIPE_CUSTOMER_ID, is_suspicious=False) self.payday.start() actual = self.payday.charge(bob, Decimal('1.00')) assert actual is None, actual @mock.patch('gittip.billing.payday.Payday.charge_on_balanced') def test_charge_success_returns_None(self, charge_on_balanced): charge_on_balanced.return_value = (Decimal('10.00'), Decimal('0.68'), "") bob = self.make_participant( 'bob', last_bill_result="failure", balanced_account_uri=self.balanced_account_uri, stripe_customer_id=self.STRIPE_CUSTOMER_ID, is_suspicious=False) self.payday.start() actual = self.payday.charge(bob, Decimal('1.00')) assert actual is None, actual @mock.patch('gittip.billing.payday.Payday.charge_on_balanced') def test_charge_success_updates_participant(self, cob): cob.return_value = (Decimal('10.00'), Decimal('0.68'), "") bob = self.make_participant( 'bob', last_bill_result="failure", balanced_account_uri=self.balanced_account_uri, is_suspicious=False) self.payday.start() self.payday.charge(bob, Decimal('1.00')) bob = Participant.from_username('bob') expected = {'balance': Decimal('9.32'), 'last_bill_result': ''} actual = { 'balance': bob.balance, 'last_bill_result': bob.last_bill_result } assert_equals(actual, expected) @mock.patch('gittip.billing.payday.Payday.charge_on_balanced') def test_payday_moves_money(self, charge_on_balanced): charge_on_balanced.return_value = (Decimal('10.00'), Decimal('0.68'), "") day_ago = utcnow() - timedelta(days=1) bob = self.make_participant('bob', claimed_time=day_ago, last_bill_result='', is_suspicious=False) carl = self.make_participant( 'carl', claimed_time=day_ago, balanced_account_uri=self.balanced_account_uri, last_bill_result='', is_suspicious=False) carl.set_tip_to('bob', '6.00') # under $10! self.payday.run() bob = Participant.from_username('bob') carl = Participant.from_username('carl') assert_equals(bob.balance, Decimal('6.00')) assert_equals(carl.balance, Decimal('3.32')) @mock.patch('gittip.billing.payday.Payday.charge_on_balanced') def test_payday_doesnt_move_money_from_a_suspicious_account( self, charge_on_balanced): charge_on_balanced.return_value = (Decimal('10.00'), Decimal('0.68'), "") day_ago = utcnow() - timedelta(days=1) bob = self.make_participant('bob', claimed_time=day_ago, last_bill_result='', is_suspicious=False) carl = self.make_participant( 'carl', claimed_time=day_ago, balanced_account_uri=self.balanced_account_uri, last_bill_result='', is_suspicious=True) carl.set_tip_to('bob', '6.00') # under $10! self.payday.run() bob = Participant.from_username('bob') carl = Participant.from_username('carl') assert_equals(bob.balance, Decimal('0.00')) assert_equals(carl.balance, Decimal('0.00')) @mock.patch('gittip.billing.payday.Payday.charge_on_balanced') def test_payday_doesnt_move_money_to_a_suspicious_account( self, charge_on_balanced): charge_on_balanced.return_value = (Decimal('10.00'), Decimal('0.68'), "") day_ago = utcnow() - timedelta(days=1) bob = self.make_participant('bob', claimed_time=day_ago, last_bill_result='', is_suspicious=True) carl = self.make_participant( 'carl', claimed_time=day_ago, balanced_account_uri=self.balanced_account_uri, last_bill_result='', is_suspicious=False) carl.set_tip_to('bob', '6.00') # under $10! self.payday.run() bob = Participant.from_username('bob') carl = Participant.from_username('carl') assert_equals(bob.balance, Decimal('0.00')) assert_equals(carl.balance, Decimal('0.00'))