def test_payday_tries_to_settle_debts(self): # First, test a small debt which can be settled e1_id = self.make_exchange('mango-cc', 10, 0, self.janet) debt = create_debt(self.db, self.janet.id, self.homer.id, EUR(5), e1_id) e2_id = self.make_exchange('mango-cc', 20, 0, self.janet) Payday.start().run() balances = dict(self.db.all("SELECT username, balance FROM participants")) assert balances == { 'janet': 25, 'homer': 5, 'david': 0, } debt = self.db.one("SELECT * FROM debts WHERE id = %s", (debt.id,)) assert debt.status == 'paid' # Second, test a big debt that can't be settled self.make_exchange('mango-ba', -15, 0, self.janet) debt2 = create_debt(self.db, self.janet.id, self.homer.id, EUR(20), e2_id) Payday.start().run() balances = dict(self.db.all("SELECT username, balance FROM participants")) assert balances == { 'janet': 10, 'homer': 5, 'david': 0, } debt2 = self.db.one("SELECT * FROM debts WHERE id = %s", (debt2.id,)) assert debt2.status == 'due'
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
def test_takes_paid_in_advance(self): team = self.make_participant( 'team', kind='group', accepted_currencies='EUR,USD' ) alice = self.make_participant('alice', main_currency='EUR', accepted_currencies='EUR,USD') team.set_take_for(alice, EUR('1.00'), team) bob = self.make_participant('bob', main_currency='USD', accepted_currencies='EUR,USD') team.set_take_for(bob, EUR('1.00'), team) stripe_account_alice = self.add_payment_account(alice, 'stripe', default_currency='EUR') self.add_payment_account(bob, 'stripe', default_currency='USD') carl = self.make_participant('carl') carl.set_tip_to(team, EUR('10')) carl_card = ExchangeRoute.insert( carl, 'stripe-card', 'x', 'chargeable', remote_user_id='x' ) payin, pt = self.make_payin_and_transfer(carl_card, team, EUR('10')) assert pt.destination == stripe_account_alice.pk Payday.start().run() transfers = self.db.all("SELECT * FROM transfers ORDER BY id") assert len(transfers) == 1 assert transfers[0].virtual is True assert transfers[0].tipper == carl.id assert transfers[0].tippee == alice.id assert transfers[0].amount == EUR('1')
def test_create_payday_issue(self, api_request, api_get, date): date.today.return_value.isoweekday.return_value = 3 # 1st payday issue api_get.return_value.json = lambda: [] repo = self.website.app_conf.payday_repo html_url = 'https://github.com/%s/issues/1' % repo api_request.return_value.json = lambda: {'html_url': html_url} create_payday_issue() args = api_request.call_args post_path = '/repos/%s/issues' % repo assert args[0] == ('POST', '', post_path) assert args[1]['json'] == {'body': '', 'title': 'Payday #1', 'labels': ['Payday']} assert args[1]['sess'].auth public_log = self.db.one("SELECT public_log FROM paydays") assert public_log == html_url api_request.reset_mock() # Check that executing the function again doesn't create a duplicate create_payday_issue() assert api_request.call_count == 0 # Run 1st payday Payday.start().run() # 2nd payday issue api_get.return_value.json = lambda: [{'body': 'Lorem ipsum', 'title': 'Payday #1'}] html_url = 'https://github.com/%s/issues/2' % repo api_request.return_value.json = lambda: {'html_url': html_url} create_payday_issue() args = api_request.call_args assert args[0] == ('POST', '', post_path) assert args[1]['json'] == {'body': 'Lorem ipsum', 'title': 'Payday #2', 'labels': ['Payday']} assert args[1]['sess'].auth public_log = self.db.one("SELECT public_log FROM paydays WHERE id = 2") assert public_log == html_url
def main(override_payday_checks=False): from liberapay.billing.transactions import sync_with_mangopay from liberapay.main import website # https://github.com/liberapay/salon/issues/19#issuecomment-191230689 from liberapay.billing.payday import Payday if not website.env.override_payday_checks and not override_payday_checks: # Check that payday hasn't already been run this week r = website.db.one(""" SELECT id FROM paydays WHERE ts_start >= now() - INTERVAL '6 days' AND ts_end >= ts_start """) assert not r, "payday has already been run this week" # Prevent a race condition, by acquiring a DB lock conn = website.db.get_connection().__enter__() cursor = conn.cursor() lock = cursor.one("SELECT pg_try_advisory_lock(1)") assert lock, "failed to acquire the payday lock" try: sync_with_mangopay(website.db) Payday.start().run(website.env.log_dir, website.env.keep_payday_logs) except KeyboardInterrupt: # pragma: no cover pass except Exception as e: # pragma: no cover website.tell_sentry(e, {}, allow_reraise=False) raise finally: conn.close()
def payday(): # Wire things up. # =============== env = wireup.env() db = wireup.db(env) wireup.billing(env) # Lazily import the billing module. # ================================= from liberapay.billing.exchanges import sync_with_mangopay from liberapay.billing.payday import Payday try: sync_with_mangopay(db) Payday.start().run() except KeyboardInterrupt: pass except: import aspen import traceback aspen.log(traceback.format_exc())
def test_it_notifies_participants(self): self.make_exchange('mango-cc', 10, 0, self.janet) self.janet.set_tip_to(self.david, '4.50') self.janet.set_tip_to(self.homer, '3.50') team = self.make_participant('team', kind='group', email='*****@*****.**') self.janet.set_tip_to(team, '0.25') team.add_member(self.david) team.set_take_for(self.david, D('0.23'), team) self.client.POST('/homer/emails/notifications.json', auth_as=self.homer, data={'fields': 'income', 'income': ''}, xhr=True) kalel = self.make_participant( 'kalel', mangopay_user_id=None, email='*****@*****.**', ) self.janet.set_tip_to(kalel, '0.10') Payday.start().run() david = self.david.refetch() assert david.balance == D('4.73') janet = self.janet.refetch() assert janet.balance == D('1.77') assert janet.giving == D('0.25') emails = self.get_emails() assert len(emails) == 3 assert emails[0]['to'][0] == 'david <%s>' % self.david.email assert '4.73' in emails[0]['subject'] assert emails[1]['to'][0] == 'kalel <%s>' % kalel.email assert 'identity form' in emails[1]['text'] assert emails[2]['to'][0] == 'janet <%s>' % self.janet.email assert 'top up' in emails[2]['subject'] assert '1.77' in emails[2]['text']
def test_update_cached_amounts_depth(self): # This test is currently broken, but we may be able to fix it someday return alice = self.make_participant('alice', balance=EUR(100)) usernames = ('bob', 'carl', 'dana', 'emma', 'fred', 'greg') users = [self.make_participant(username) for username in usernames] prev = alice for user in reversed(users): prev.set_tip_to(user, EUR('1.00')) prev = user 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 check() Payday.start().update_cached_amounts() check()
def test_wellfunded_team(self): """ This tests two properties: - takes are maximums - donors all pay their share, the first donor doesn't pay everything """ team = self.make_participant('team', kind='group') alice = self.make_participant('alice') team.set_take_for(alice, EUR('0.79'), team) bob = self.make_participant('bob') team.set_take_for(bob, EUR('0.21'), team) charlie = self.make_participant('charlie', balance=EUR(10)) charlie.set_tip_to(team, EUR('5.00')) dan = self.make_participant('dan', balance=EUR(10)) dan.set_tip_to(team, EUR('5.00')) Payday.start().run() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.79'), 'bob': EUR('0.21'), 'charlie': EUR('9.5'), 'dan': EUR('9.5'), 'team': EUR('0.00'), } assert d == expected
def test_update_cached_amounts(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') carl = self.make_participant('carl', balance=1.56) dana = self.make_participant('dana') emma = Participant.make_stub(username='******') alice.set_tip_to(dana, '3.00') alice.set_tip_to(bob, '6.00') alice.set_tip_to(emma, '1.00') alice.set_tip_to(team, '4.00') bob.set_tip_to(alice, '5.00') team.add_member(bob) team.set_take_for(bob, D('1.00'), bob) bob.set_tip_to(dana, '2.00') # funded by bob's take bob.set_tip_to(emma, '7.00') # not funded, insufficient receiving carl.set_tip_to(dana, '2.08') # not funded, insufficient balance def check(): alice = Participant.from_username('alice') bob = Participant.from_username('bob') carl = Participant.from_username('carl') dana = Participant.from_username('dana') emma = Participant.from_username('emma') assert alice.giving == D('13.00') assert alice.pledging == D('1.00') assert alice.receiving == D('5.00') assert bob.giving == D('7.00') assert bob.receiving == D('7.00') assert bob.taking == D('1.00') assert carl.giving == D('0.00') assert carl.receiving == D('0.00') assert dana.receiving == D('5.00') assert dana.npatrons == 2 assert emma.receiving == D('1.00') assert emma.npatrons == 1 funded_tips = self.db.all("SELECT amount FROM tips WHERE is_funded ORDER BY id") assert funded_tips == [3, 6, 1, 4, 5, 2] # Pre-test check check() # Check that update_cached_amounts doesn't mess anything up Payday.start().update_cached_amounts() check() # Check that update_cached_amounts actually updates amounts self.db.run(""" UPDATE tips SET is_funded = false; UPDATE participants SET giving = 0 , npatrons = 0 , pledging = 0 , receiving = 0 , taking = 0; """) Payday.start().update_cached_amounts() check()
def test_payday_doesnt_make_null_transfers(self): alice = self.make_participant('alice') alice.set_tip_to(self.homer, EUR('1.00')) alice.set_tip_to(self.homer, EUR(0)) a_team = self.make_participant('a_team', kind='group') a_team.add_member(alice) Payday.start().run() transfers0 = self.db.all("SELECT * FROM transfers WHERE amount = 0") assert not transfers0
def test_dispute_callback_lost(self, save, get_payin, get_dispute): self.make_participant( 'LiberapayOrg', kind='organization', balance=D('100.00'), mangopay_user_id='0', mangopay_wallet_id='0', ) save.side_effect = fake_transfer e_id = self.make_exchange('mango-cc', D('16'), D('1'), self.janet) dispute = Dispute() dispute.Id = '-1' dispute.CreationDate = utcnow() dispute.DisputedFunds = Money(1700, 'EUR') dispute.DisputeType = 'CONTESTABLE' dispute.InitialTransactionType = 'PAYIN' get_dispute.return_value = dispute payin = PayIn(tag=str(e_id)) get_payin.return_value = payin # Transfer some of the money to homer self.janet.set_tip_to(self.homer, EUR('3.68')) Payday.start().run() # Withdraw some of the money self.make_exchange('mango-ba', D('-2.68'), 0, self.homer) # Add a bit of money that will remain undisputed, to test bundle swapping self.make_exchange('mango-cc', D('0.32'), 0, self.janet) self.make_exchange('mango-cc', D('0.55'), 0, self.homer) # Call back self.db.self_check() for status in ('CREATED', 'CLOSED'): dispute.Status = status if status == 'CLOSED': dispute.ResultCode = 'LOST' qs = "EventType=DISPUTE_"+status+"&RessourceId=123456790" r = self.callback(qs, raise_immediately=True) assert r.code == 200, r.text self.db.self_check() # Check final state balances = dict(self.db.all("SELECT username, balance FROM participants")) assert balances == { '_chargebacks_': D('16.00'), 'david': 0, 'homer': 0, 'janet': 0, 'LiberapayOrg': D('98.19'), } debts = dict(((r[0], r[1]), r[2]) for r in self.db.all(""" SELECT p_debtor.username AS debtor, p_creditor.username AS creditor, sum(d.amount) FROM debts d JOIN participants p_debtor ON p_debtor.id = d.debtor JOIN participants p_creditor ON p_creditor.id = d.creditor WHERE d.status = 'due' GROUP BY p_debtor.username, p_creditor.username """)) assert debts == { ('janet', 'LiberapayOrg'): D('1.00'), ('janet', 'homer'): D('3.36'), ('homer', 'LiberapayOrg'): D('1.81'), }
def take_last_week(self, team, member, amount, actual_amount=None): team.set_take_for(member, amount, team, check_max=False) Payday.start() actual_amount = amount if actual_amount is None else actual_amount if D(actual_amount) > 0: self.db.run(""" INSERT INTO transfers (tipper, tippee, amount, context, status, team, wallet_from, wallet_to) VALUES (%(tipper)s, %(tippee)s, %(amount)s, 'take', 'succeeded', %(team)s, '-1', '-2') """, dict(tipper=self.warbucks.id, tippee=member.id, amount=EUR(actual_amount), team=team.id)) self.db.run("UPDATE paydays SET ts_end=now() WHERE ts_end < ts_start")
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_iter_payday_events(self): Payday.start().run() team = self.make_participant('team', kind='group') alice = self.make_participant('alice') self.make_exchange('mango-cc', 10000, 0, alice) self.make_exchange('mango-cc', -5000, 0, alice) self.db.run(""" UPDATE transfers SET timestamp = "timestamp" - interval '1 month' """) bob = self.make_participant('bob') carl = self.make_participant('carl') david = self.make_participant('david') self.make_exchange('mango-cc', 10000, 0, david) david.set_tip_to(team, Decimal('1.00')) team.set_take_for(bob, Decimal('1.00'), bob) alice.set_tip_to(bob, Decimal('5.00')) assert bob.balance == 0 for i in range(2): 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) == 9 assert events[0]['kind'] == 'totals' assert events[0]['given'] == 0 assert events[0]['received'] == 12 assert events[1]['kind'] == 'day-open' assert events[1]['payday_number'] == 2 assert events[2]['balance'] == 12 assert events[-1]['kind'] == 'day-close' assert events[-1]['balance'] == 0 alice = Participant.from_id(alice.id) assert alice.balance == 4990 events = list(iter_payday_events(self.db, alice)) assert events[0]['given'] == 10 assert len(events) == 11 carl = Participant.from_id(carl.id) assert carl.balance == 0 events = list(iter_payday_events(self.db, carl)) assert len(events) == 0
def test_it_notifies_participants(self): self.make_exchange('mango-cc', 10, 0, self.janet) self.janet.set_tip_to(self.david, '4.50') self.janet.set_tip_to(self.homer, '3.50') self.client.POST('/homer/emails/notifications.json', auth_as=self.homer, data={'fields': 'income', 'income': ''}, xhr=True) Payday.start().run() emails = self.get_emails() assert len(emails) == 2 assert emails[0]['to'][0]['email'] == self.david.email assert '4.50' in emails[0]['subject'] assert emails[1]['to'][0]['email'] == self.janet.email assert 'top up' in emails[1]['subject']
def test_payday_can_be_restarted_after_crash(self, transfer_for_real, exec_payday): transfer_for_real.side_effect = Foobar self.janet.set_tip_to(self.homer, EUR('6.00')) with self.assertRaises(Foobar): Payday.start().run() # Check that the web interface allows relaunching admin = self.make_participant('admin', privileges=1) r = self.client.PxST('/admin/payday', data={'action': 'rerun_payday'}, auth_as=admin) assert r.code == 302 assert exec_payday.call_count == 1 # Test actually relaunching transfer_for_real.side_effect = None Payday.start().run()
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_gtlwf_works_during_payday(self): team, alice = self.make_team_of_one() self.take_last_week(team, alice, '20.00') assert team.get_takes_last_week()[alice.id] == 20 self.take_last_week(team, alice, '30.00') assert team.get_takes_last_week()[alice.id] == 30 take_this_week = D('42.00') team.set_take_for(alice, take_this_week, alice) Payday.start() assert team.get_takes_last_week()[alice.id] == 30 self.db.run(""" INSERT INTO transfers (tipper, tippee, amount, context, status, team, wallet_from, wallet_to) VALUES (%(tipper)s, %(id)s, %(amount)s, 'take', 'succeeded', %(team)s, '-1', '-2') """, dict(tipper=self.warbucks.id, id=alice.id, amount=EUR(take_this_week), team=team.id)) assert team.get_takes_last_week()[alice.id] == 30
def test_wellfunded_team_with_two_early_donors_and_low_amounts(self): self.clear_tables() team = self.make_participant('team', kind='group') alice = self.make_participant('alice') team.set_take_for(alice, D('0.01'), team) bob = self.make_participant('bob') team.set_take_for(bob, D('0.01'), team) charlie = self.make_participant('charlie', balance=10) charlie.set_tip_to(team, EUR('0.02')) dan = self.make_participant('dan', balance=10) dan.set_tip_to(team, EUR('0.02')) print("> Step 1: three weeks of donations from early donors") print() for i in range(3): Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': D('0.01') * 3, 'bob': D('0.01') * 3, 'charlie': D('9.97'), 'dan': D('9.97'), 'team': D('0.00'), } assert d == expected print("> Step 2: a new donor appears, the contributions of the early " "donors automatically decrease while the new donor catches up") print() emma = self.make_participant('emma', balance=10) emma.set_tip_to(team, EUR('0.02')) for i in range(6): Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': D('0.01') * 9, 'bob': D('0.01') * 9, 'charlie': D('9.94'), 'dan': D('9.94'), 'emma': D('9.94'), 'team': D('0.00'), } assert d == expected
def test_transfer_takes(self): a_team = self.make_participant('a_team', kind='group') alice = self.make_participant('alice') a_team.set_take_for(alice, D('1.00'), alice) bob = self.make_participant('bob') a_team.set_take_for(bob, D('0.01'), bob) charlie = self.make_participant('charlie', balance=1000) charlie.set_tip_to(a_team, D('1.01')) 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): payday.shuffle() os.unlink(payday.transfers_filename) participants = self.db.all("SELECT username, balance FROM participants") for p in participants: if p.username == 'alice': assert p.balance == D('1.00') elif p.username == 'bob': assert p.balance == D('0.01') elif p.username == 'charlie': assert p.balance == D('998.99') else: assert p.balance == 0
def test_transfer_takes(self): a_team = self.make_participant('a_team', kind='group', balance=20) alice = self.make_participant('alice') a_team.add_member(alice) a_team.add_member(self.make_participant('bob')) 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): payday.shuffle() 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_log_upload(self): payday = Payday.start() with open('payday-%i.txt.part' % payday.id, 'w') as f: f.write('fake log file\n') with mock.patch.object(self.website, 's3') as s3: payday.run('.', keep_log=True) assert s3.upload_file.call_count == 1
def test_unfunded_tip_to_team_doesnt_cause_NegativeBalance(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice') alice.set_tip_to(team, EUR('1.00')) # unfunded tip bob = self.make_participant('bob') team.set_take_for(bob, EUR('1.00'), team) Payday.start().run() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.00'), 'bob': EUR('0.00'), 'team': EUR('0.00'), } assert d == expected
def test_transfer_takes(self): a_team = self.make_participant('a_team', kind='group') alice = self.make_participant('alice') a_team.set_take_for(alice, EUR('1.00'), a_team) bob = self.make_participant('bob') a_team.set_take_for(bob, EUR('0.01'), a_team) charlie = self.make_participant('charlie', balance=EUR(1000)) charlie.set_tip_to(a_team, EUR('1.01')) payday = Payday.start() # Test that payday ignores takes set after it started a_team.set_take_for(alice, EUR('2.00'), a_team) # Run the transfer multiple times to make sure we ignore takes that # have already been processed with mock.patch.object(payday, 'transfer_for_real') as f: f.side_effect = Foobar with self.assertRaises(Foobar): payday.shuffle() for i in range(2): payday.shuffle() participants = self.db.all("SELECT username, balance FROM participants") for p in participants: if p.username == 'alice': assert p.balance == EUR('1.00') elif p.username == 'bob': assert p.balance == EUR('0.01') elif p.username == 'charlie': assert p.balance == EUR('998.99') else: assert p.balance == 0
def test_start_prepare(self, log): self.clear_tables() self.make_participant('carl', balance=EUR(10)) 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 = [ ('Running payday #1.'), ('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 participant assert len(participants) == 1 assert participants == second_participants expected_logging_call_args = [ ('Running payday #1.'), ('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_wellfunded_team_with_early_donor_and_small_leftover(self): self.clear_tables() team = self.make_participant('team', kind='group') alice = self.make_participant('alice') team.set_take_for(alice, D('0.50'), team) bob = self.make_participant('bob') team.set_take_for(bob, D('0.50'), team) charlie = self.make_participant('charlie', balance=10) charlie.set_tip_to(team, EUR('0.52')) print("> Step 1: three weeks of donations from early donor") print() for i in range(3): Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': D('0.26') * 3, 'bob': D('0.26') * 3, 'charlie': D('8.44'), 'team': D('0.00'), } assert d == expected print("> Step 2: a new donor appears, the contribution of the early " "donor automatically decreases while the new donor catches up, " "but the leftover is small so the adjustments are limited") print() dan = self.make_participant('dan', balance=10) dan.set_tip_to(team, EUR('0.52')) for i in range(3): Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': D('0.26') * 3 + D('0.50') * 3, 'bob': D('0.26') * 3 + D('0.50') * 3, 'charlie': D('7.00'), 'dan': D('8.44'), 'team': D('0.00'), } assert d == expected
def test_mutual_tipping_through_teams(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice', balance=EUR(8)) alice.set_tip_to(team, EUR('2.00')) team.set_take_for(alice, EUR('0.25'), team) bob = self.make_participant('bob', balance=EUR(10)) bob.set_tip_to(team, EUR('2.00')) team.set_take_for(bob, EUR('0.75'), team) Payday.start().run() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('7.75'), # 8 - 0.50 + 0.25 'bob': EUR('10.25'), # 10 - 0.25 + 0.50 'team': EUR('0.00'), } assert d == expected
def test_user_page_shows_pledges(self, get_user_info): alice = self.make_elsewhere('github', 1, 'alice').participant bob = self.make_participant('bob') carl = self.make_participant('carl') # bob needs to be an active donor for his pledge to be counted bob.set_tip_to(carl, EUR('1.00')) bob_card = ExchangeRoute.insert( bob, 'stripe-card', 'x', 'chargeable', remote_user_id='x' ) self.add_payment_account(carl, 'stripe') self.make_payin_and_transfer(bob_card, carl, EUR('1.00')) Payday.start().run() # okay, let's check amount = EUR('14.97') bob.set_tip_to(alice, amount) assert alice.receiving == amount r = self.client.GET('/on/github/alice/') assert str(amount.amount) in r.text, r.text
def test_it_handles_invoices_correctly(self): org = self.make_participant('org', kind='organization', allow_invoices=True) self.make_exchange('mango-cc', 60, 0, self.janet) self.janet.set_tip_to(org, EUR('50.00')) self.db.run("UPDATE participants SET allow_invoices = true WHERE id = %s", (self.janet.id,)) self.make_invoice(self.janet, org, '40.02', 'accepted') self.make_invoice(self.janet, org, '80.04', 'accepted') self.make_invoice(self.janet, org, '5.16', 'rejected') self.make_invoice(self.janet, org, '3.77', 'new') self.make_invoice(self.janet, org, '1.23', 'pre') Payday.start().run() expense_transfers = self.db.all("SELECT * FROM transfers WHERE context = 'expense'") assert len(expense_transfers) == 1 d = dict(self.db.all("SELECT username, balance FROM participants WHERE balance <> 0")) assert d == { 'org': EUR('9.98'), 'janet': EUR('50.02'), }
def test_transfer_tips_whole_graph(self): alice = self.make_participant('alice', balance=EUR(50)) alice.set_tip_to(self.homer, EUR('50')) self.homer.set_tip_to(self.janet, EUR('20')) self.janet.set_tip_to(self.david, EUR('5')) self.david.set_tip_to(self.homer, EUR('20')) # Should be unfunded payday = Payday.start() with self.db.get_cursor() as cursor: payday.prepare(cursor, payday.ts_start) payday.transfer_virtually(cursor, payday.ts_start) new_balances = self.get_new_balances(cursor) assert new_balances[alice.id] == [] assert new_balances[self.homer.id] == [EUR('30')] assert new_balances[self.janet.id] == [EUR('15')] assert new_balances[self.david.id] == [EUR('5')]
def test_transfer_tips_whole_graph(self): alice = self.make_participant('alice', balance=EUR(50)) alice.set_tip_to(self.homer, EUR('50')) self.homer.set_tip_to(self.janet, EUR('20')) self.janet.set_tip_to(self.david, EUR('5')) self.david.set_tip_to(self.homer, EUR('20')) # Partially funded payday = Payday.start() with self.db.get_cursor() as cursor: payday.prepare(cursor, payday.ts_start) payday.transfer_virtually(cursor, payday.ts_start) new_balances = self.get_new_balances(cursor) assert new_balances == { 'alice': [], 'david': [], 'homer': [EUR('35')], 'janet': [EUR('15')], }
def test_payday_can_be_resumed_at_any_stage(self): payday = Payday.start() with mock.patch.object(Payday, 'clean_up') as f: f.side_effect = Foobar with self.assertRaises(Foobar): payday.run() assert payday.stage == 2 with mock.patch.object(Payday, 'recompute_stats') as f: f.side_effect = Foobar with self.assertRaises(Foobar): payday.run() assert payday.stage == 3 with mock.patch('liberapay.payin.cron.send_donation_reminder_notifications') as f: f.side_effect = Foobar with self.assertRaises(Foobar): payday.run() assert payday.stage == 4 with mock.patch.object(Payday, 'generate_payment_account_required_notifications') as f: f.side_effect = Foobar with self.assertRaises(Foobar): payday.run() assert payday.stage == 5 payday.run() assert payday.stage is None
def test_transfer_tips(self): self.make_exchange('mango-cc', 1, 0, self.david) self.david.set_tip_to(self.janet, EUR('0.51')) self.david.set_tip_to(self.homer, EUR('0.50')) payday = Payday.start() with self.db.get_cursor() as cursor: payday.prepare(cursor, payday.ts_start) payday.transfer_virtually(cursor, payday.ts_start) new_balances = self.get_new_balances(cursor) assert new_balances == { 'david': [], 'homer': [EUR('0.49')], 'janet': [EUR('0.51')], } tips = dict(cursor.all("SELECT tippee, is_funded FROM payday_tips")) assert tips == { self.janet.id: True, self.homer.id: False, } transfers = dict(cursor.all("SELECT tippee, context FROM payday_transfers")) assert transfers == { self.janet.id: 'tip', self.homer.id: 'partial-tip', }
def test_end(self): Payday.start().end() result = self.db.one("SELECT count(*) FROM paydays " "WHERE ts_end > '1970-01-01'") assert result == 1
def test_update_cached_amounts(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice') alice_card = self.upsert_route(alice, 'stripe-card') bob = self.make_participant('bob') carl = self.make_participant('carl') carl_card = self.upsert_route(carl, 'stripe-card') dana = self.make_participant('dana') emma = Participant.make_stub(username='******') team2 = self.make_participant('team2', kind='group') team2.add_member(dana) alice.set_tip_to(dana, EUR('3.00')) self.make_payin_and_transfer(alice_card, dana, EUR('30.00')) alice.set_tip_to(bob, EUR('6.00')) self.make_payin_and_transfer(alice_card, bob, EUR('60.00')) alice.set_tip_to(emma, EUR('0.50')) alice.set_tip_to(team, EUR('1.20')) alice.set_tip_to(team2, EUR('0.49')) self.make_payin_and_transfer(alice_card, team2, EUR('4.90')) bob.set_tip_to(alice, EUR('5.00')) team.set_take_for(bob, EUR('1.00'), team) self.make_payin_and_transfer(alice_card, team, EUR('12.00')) bob.set_tip_to(dana, EUR('2.00')) # funded by bob's take bob.set_tip_to(emma, EUR('7.00')) # not funded, insufficient receiving carl.set_tip_to(dana, EUR('2.08')) # not funded, insufficient balance self.make_payin_and_transfer(carl_card, dana, EUR('1.56')) fred = self.make_participant('fred') fred_card = self.upsert_route(fred, 'stripe-card') fred.set_tip_to(dana, EUR('2.22')) self.make_payin_and_transfer(fred_card, dana, EUR('8.88')) self.db.run( "UPDATE participants SET is_suspended = true WHERE username = '******'" ) dana.update_receiving() def check(): alice = Participant.from_username('alice') bob = Participant.from_username('bob') carl = Participant.from_username('carl') dana = Participant.from_username('dana') emma = Participant.from_username('emma') assert alice.giving == EUR('10.69') assert alice.receiving == EUR('0.00') assert alice.npatrons == 0 assert alice.nteampatrons == 0 assert bob.giving == EUR('0.00') assert bob.taking == EUR('1.00') assert bob.receiving == EUR('7.00') assert bob.npatrons == 1 assert bob.nteampatrons == 1 assert carl.giving == EUR('0.00') assert carl.receiving == EUR('0.00') assert carl.npatrons == 0 assert carl.nteampatrons == 0 assert dana.receiving == EUR('3.49') assert dana.npatrons == 1 assert dana.nteampatrons == 1 assert emma.receiving == EUR('0.50') assert emma.npatrons == 1 assert emma.nteampatrons == 0 funded_tips = self.db.all( "SELECT amount FROM tips WHERE is_funded ORDER BY id") assert funded_tips == [ 3, 6, 0.5, EUR('1.20'), EUR('0.49'), EUR('2.22') ] team = Participant.from_username('team') assert team.receiving == EUR('1.20') assert team.npatrons == 1 assert team.leftover == EUR('0.20') team2 = Participant.from_username('team2') assert team2.receiving == EUR('0.49') assert team2.npatrons == 1 assert team2.leftover == EUR('0.00') janet = self.janet.refetch() assert janet.giving == 0 assert janet.receiving == 0 assert janet.taking == 0 assert janet.npatrons == 0 assert janet.nteampatrons == 0 # Pre-test check check() # Check that update_cached_amounts doesn't mess anything up Payday.start().update_cached_amounts() check() # Check that update_cached_amounts actually updates amounts self.db.run(""" UPDATE tips t SET is_funded = true FROM participants p WHERE p.id = t.tippee AND p.mangopay_user_id IS NOT NULL; UPDATE participants SET giving = (10000,'EUR') , taking = (10000,'EUR') WHERE mangopay_user_id IS NOT NULL; UPDATE participants SET npatrons = 10000 , receiving = (10000,'EUR'); """) Payday.start().update_cached_amounts() check() # Check that the update methods of Participant concur for p in self.db.all("SELECT p.*::participants FROM participants p"): p.update_receiving() p.update_giving() check()
def test_can_post_to_close_page_during_payday(self): Payday.start() alice = self.make_participant('alice') response = self.client.PxST('/alice/settings/close', auth_as=alice) assert response.code == 302 assert response.headers[b'Location'] == b'/alice/'
def test_update_cached_amounts(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') carl = self.make_participant('carl', balance=1.56) dana = self.make_participant('dana') emma = Participant.make_stub(username='******') team2 = self.make_participant('team2', kind='group') team2.add_member(dana) alice.set_tip_to(dana, '3.00') alice.set_tip_to(bob, '6.00') alice.set_tip_to(emma, '0.50') alice.set_tip_to(team, '1.20') alice.set_tip_to(team2, '0.49') bob.set_tip_to(alice, '5.00') team.add_member(bob) team.set_take_for(bob, D('1.00'), team) bob.set_tip_to(dana, '2.00') # funded by bob's take bob.set_tip_to(emma, '7.00') # not funded, insufficient receiving carl.set_tip_to(dana, '2.08') # not funded, insufficient balance def check(): alice = Participant.from_username('alice') bob = Participant.from_username('bob') carl = Participant.from_username('carl') dana = Participant.from_username('dana') emma = Participant.from_username('emma') assert alice.giving == D('10.69') assert alice.receiving == D('5.00') assert bob.giving == D('7.00') assert bob.receiving == D('7.00') assert bob.taking == D('1.00') assert carl.giving == D('0.00') assert carl.receiving == D('0.00') assert dana.receiving == D('5.00') assert dana.npatrons == 2 assert emma.receiving == D('0.50') assert emma.npatrons == 1 funded_tips = self.db.all( "SELECT amount FROM tips WHERE is_funded ORDER BY id") assert funded_tips == [3, 6, 0.5, D('1.20'), D('0.49'), 5, 2] team = Participant.from_username('team') assert team.receiving == D('1.20') assert team.npatrons == 1 team2 = Participant.from_username('team2') assert team2.receiving == D('0.49') assert team2.npatrons == 1 janet = self.janet.refetch() assert janet.giving == 0 assert janet.receiving == 0 assert janet.taking == 0 assert janet.npatrons == 0 # Pre-test check check() # Check that update_cached_amounts doesn't mess anything up Payday.start().update_cached_amounts() check() # Check that update_cached_amounts actually updates amounts self.db.run(""" UPDATE tips t SET is_funded = true FROM participants p WHERE p.id = t.tippee AND p.mangopay_user_id IS NOT NULL; UPDATE participants SET giving = 10000 , taking = 10000 WHERE mangopay_user_id IS NOT NULL; UPDATE participants SET npatrons = 10000 , receiving = 10000 WHERE status = 'active'; """) Payday.start().update_cached_amounts() check() # Check that the update methods of Participant concur for p in self.db.all("SELECT p.*::participants FROM participants p"): p.update_receiving() p.update_giving() check()
def run_payday(self): Payday.start().run()
def test_wellfunded_team_with_two_early_donors(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice') team.set_take_for(alice, EUR('0.79'), team) bob = self.make_participant('bob') team.set_take_for(bob, EUR('0.21'), team) charlie = self.make_participant('charlie', balance=EUR(10)) charlie.set_tip_to(team, EUR('1.00')) dan = self.make_participant('dan', balance=EUR(10)) dan.set_tip_to(team, EUR('3.00')) print("> Step 1: three weeks of donations from early donors") print() for i in range(3): Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.79') * 3, 'bob': EUR('0.21') * 3, 'charlie': EUR('9.25'), 'dan': EUR('7.75'), 'team': EUR('0.00'), } assert d == expected print("> Step 2: a new donor appears, the contributions of the early " "donors automatically decrease while the new donor catches up") print() emma = self.make_participant('emma', balance=EUR(10)) emma.set_tip_to(team, EUR('1.00')) Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.79') * 4, 'bob': EUR('0.21') * 4, 'charlie': EUR('9.19'), 'dan': EUR('7.59'), 'emma': EUR('9.22'), 'team': EUR('0.00'), } assert d == expected Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.79') * 5, 'bob': EUR('0.21') * 5, 'charlie': EUR('8.99'), 'dan': EUR('7.01'), 'emma': EUR('9.00'), 'team': EUR('0.00'), } assert d == expected print("> Step 3: emma has caught up with the early donors") print() for i in range(2): Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.79') * 7, 'bob': EUR('0.21') * 7, 'charlie': EUR('8.60'), 'dan': EUR('5.80'), 'emma': EUR('8.60'), 'team': EUR('0.00'), } assert d == expected
def test_payout_during_payday(self): self.make_exchange('mango-cc', 200, 0, self.homer) Payday.start() with self.assertRaises(PaydayIsRunning): payout(self.db, self.homer_route, EUR('97.35'))
def test_payday_handles_paid_in_advance(self): self.make_exchange('mango-cc', EUR('2.00'), 0, self.janet) self.janet.set_tip_to(self.david, EUR('0.60')) team = self.make_participant('team', kind='group') team.set_take_for(self.homer, EUR('0.40'), team) self.janet.set_tip_to(team, EUR('0.40')) self.janet.distribute_balances_to_donees(final_gift=False) # Preliminary checks janet = self.janet.refetch() assert janet.balance == 0 tips = self.db.all( """ SELECT * FROM current_tips WHERE tipper = %s ORDER BY id """, (janet.id, )) assert len(tips) == 2 assert tips[0].paid_in_advance == EUR('1.20') assert tips[1].paid_in_advance == EUR('0.80') transfers = self.db.all("SELECT * FROM transfers ORDER BY id") assert len(transfers) == 2 assert transfers[0].amount == EUR('1.20') assert transfers[0].context == 'tip-in-advance' assert transfers[1].amount == EUR('0.80') assert transfers[1].context == 'take-in-advance' # Now run a payday and check the results Payday.start().run() tips = self.db.all( """ SELECT * FROM current_tips WHERE tipper = %s ORDER BY id """, (janet.id, )) assert len(tips) == 2 assert tips[0].paid_in_advance == EUR('0.60') assert tips[1].paid_in_advance == EUR('0.40') transfers = self.db.all("SELECT * FROM transfers ORDER BY id") assert len(transfers) == 4 self.db.run( "UPDATE notifications SET email = true WHERE event = 'low_balance'" ) # temporary bypass emails = self.get_emails() assert len(emails) == 2 assert emails[0]['to'][0] == 'david <%s>' % self.david.email assert '0.60' in emails[0]['subject'] assert emails[1]['to'][0] == 'homer <%s>' % self.homer.email assert '0.40' in emails[1]['text'] # Now run a second payday and check the results again Payday.start().run() tips = self.db.all( """ SELECT * FROM current_tips WHERE tipper = %s ORDER BY id """, (janet.id, )) assert len(tips) == 2 assert not tips[0].paid_in_advance assert not tips[1].paid_in_advance transfers = self.db.all("SELECT * FROM transfers ORDER BY id") assert len(transfers) == 6 self.db.run( "UPDATE notifications SET email = true WHERE event = 'low_balance'" ) # temporary bypass emails = self.get_emails() assert len(emails) == 3 assert emails[0]['to'][0] == 'david <%s>' % self.david.email assert '0.60' in emails[0]['subject'] assert emails[1]['to'][0] == 'homer <%s>' % self.homer.email assert '0.40' in emails[1]['text'] assert emails[2]['to'][0] == 'janet <%s>' % self.janet.email assert 'top up' in emails[2]['subject'] assert '1.00' in emails[2]['text']
def test_update_cached_amounts(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice', balance=100) bob = self.make_participant('bob') carl = self.make_participant('carl', balance=1.56) dana = self.make_participant('dana') emma = Participant.make_stub(username='******') alice.set_tip_to(dana, '3.00') alice.set_tip_to(bob, '6.00') alice.set_tip_to(emma, '0.50') alice.set_tip_to(team, '1.00') bob.set_tip_to(alice, '5.00') team.add_member(bob) team.set_take_for(bob, D('1.00'), bob) bob.set_tip_to(dana, '2.00') # funded by bob's take bob.set_tip_to(emma, '7.00') # not funded, insufficient receiving carl.set_tip_to(dana, '2.08') # not funded, insufficient balance def check(): alice = Participant.from_username('alice') bob = Participant.from_username('bob') carl = Participant.from_username('carl') dana = Participant.from_username('dana') emma = Participant.from_username('emma') assert alice.giving == D('10.00') assert alice.receiving == D('5.00') assert bob.giving == D('7.00') assert bob.receiving == D('7.00') assert bob.taking == D('1.00') assert carl.giving == D('0.00') assert carl.receiving == D('0.00') assert dana.receiving == D('5.00') assert dana.npatrons == 2 assert emma.receiving == D('0.50') assert emma.npatrons == 1 funded_tips = self.db.all( "SELECT amount FROM tips WHERE is_funded ORDER BY id") assert funded_tips == [3, 6, 0.5, 1, 5, 2] # Pre-test check check() # Check that update_cached_amounts doesn't mess anything up Payday.start().update_cached_amounts() check() # Check that update_cached_amounts actually updates amounts self.db.run(""" UPDATE tips t SET is_funded = false FROM participants p WHERE p.id = t.tippee AND p.mangopay_user_id IS NOT NULL; UPDATE participants SET giving = 0 , npatrons = 0 , receiving = 0 , taking = 0 WHERE mangopay_user_id IS NOT NULL; """) Payday.start().update_cached_amounts() check()
def test_close_page_is_available_during_payday(self): Payday.start() alice = self.make_participant('alice') body = self.client.GET('/alice/settings/close', auth_as=alice).text assert '<h3>Ready?' in body
def run_payday(self): self.db.run("UPDATE notifications SET ts = ts - interval '1 week'") payday = Payday.start() payday.run(recompute_stats=1) return payday
def test_takes_paid_in_advance_to_now_inactive_members(self): team = self.make_participant('team', kind='group', accepted_currencies=None) alice = self.make_participant('alice', main_currency='EUR', accepted_currencies=None) team.set_take_for(alice, EUR('1.00'), team) bob = self.make_participant('bob', main_currency='USD', accepted_currencies=None) team.set_take_for(bob, USD('1.00'), team) stripe_account_alice = self.add_payment_account( alice, 'stripe', default_currency='EUR' ) stripe_account_bob = self.add_payment_account( bob, 'stripe', country='US', default_currency='USD' ) carl = self.make_participant('carl') carl.set_tip_to(team, JPY('250')) carl_card = ExchangeRoute.insert( carl, 'stripe-card', 'x', 'chargeable', remote_user_id='x' ) payin, pt = self.make_payin_and_transfer(carl_card, team, JPY('1250')) assert pt.destination == stripe_account_alice.pk payin, pt = self.make_payin_and_transfer(carl_card, team, JPY('1250')) assert pt.destination == stripe_account_bob.pk team.set_take_for(alice, EUR('0.00'), team) team.set_take_for(bob, None, team) takes = dict(self.db.all(""" SELECT DISTINCT ON (member) member, paid_in_advance FROM takes ORDER BY member, mtime DESC """)) assert takes == { alice.id: EUR('10.00'), bob.id: USD('12.00'), } Payday.start().run() transfers = self.db.all("SELECT * FROM transfers ORDER BY id") assert len(transfers) == 2 assert transfers[0].virtual is True assert transfers[0].tipper == carl.id assert transfers[0].tippee == alice.id assert transfers[0].amount == JPY('125') assert transfers[1].virtual is True assert transfers[1].tipper == carl.id assert transfers[1].tippee == bob.id assert transfers[1].amount == JPY('125') takes = dict(self.db.all(""" SELECT DISTINCT ON (member) member, paid_in_advance FROM takes ORDER BY member, mtime DESC """)) assert takes == { alice.id: EUR('9.00'), bob.id: USD('10.80'), } notifications = self.db.all("SELECT * FROM notifications") assert len(notifications) == 0 leftovers = dict(self.db.all("SELECT username, leftover FROM participants")) assert leftovers == { 'team': MoneyBasket(JPY('250.00')), 'alice': None, 'bob': None, 'carl': None, }
def test_payday_handles_paid_in_advance(self): self.add_payment_account(self.david, 'stripe') self.add_payment_account(self.homer, 'paypal') self.make_exchange('mango-cc', EUR('2.00'), 0, self.janet) self.janet.set_tip_to(self.david, EUR('0.60')) team = self.make_participant('team', kind='group') team.set_take_for(self.homer, EUR('0.40'), team) self.janet.set_tip_to(team, EUR('0.40')) self.janet.distribute_balances_to_donees() # Preliminary checks janet = self.janet.refetch() assert janet.balance == 0 tips = self.db.all( """ SELECT * FROM current_tips WHERE tipper = %s ORDER BY id """, (janet.id, )) assert len(tips) == 2 assert tips[0].paid_in_advance == EUR('1.20') assert tips[1].paid_in_advance == EUR('0.80') transfers = self.db.all("SELECT * FROM transfers ORDER BY id") assert len(transfers) == 2 assert transfers[0].amount == EUR('1.20') assert transfers[0].context == 'tip-in-advance' assert transfers[1].amount == EUR('0.80') assert transfers[1].context == 'take-in-advance' # Now run a payday and check the results self.db.run(""" UPDATE scheduled_payins SET ctime = ctime - interval '12 hours' , execution_date = current_date """) Payday.start().run() tips = self.db.all( """ SELECT * FROM current_tips WHERE tipper = %s ORDER BY id """, (janet.id, )) assert len(tips) == 2 assert tips[0].paid_in_advance == EUR('0.60') assert tips[1].paid_in_advance == EUR('0.40') transfers = self.db.all("SELECT * FROM transfers ORDER BY id") assert len(transfers) == 4 emails = self.get_emails() assert len(emails) == 3 assert emails[0]['to'][0] == 'david <%s>' % self.david.email assert '0.60' in emails[0]['subject'] assert emails[1]['to'][0] == 'homer <%s>' % self.homer.email assert '0.40' in emails[1]['text'] assert emails[2]['to'][0] == 'janet <%s>' % self.janet.email assert 'renew your donation' in emails[2]['subject'] assert '2 donations' in emails[2]['text'] # Now run a second payday and check the results again self.db.run("UPDATE notifications SET ts = ts - interval '1 week'") Payday.start().run() tips = self.db.all( """ SELECT * FROM current_tips WHERE tipper = %s ORDER BY id """, (janet.id, )) assert len(tips) == 2 assert not tips[0].paid_in_advance assert not tips[1].paid_in_advance transfers = self.db.all("SELECT * FROM transfers ORDER BY id") assert len(transfers) == 6 emails = self.get_emails() assert len(emails) == 2 assert emails[0]['to'][0] == 'david <%s>' % self.david.email assert '0.60' in emails[0]['subject'] assert emails[1]['to'][0] == 'homer <%s>' % self.homer.email assert '0.40' in emails[1]['text']
def test_wellfunded_team_with_early_donor(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice') team.set_take_for(alice, EUR('0.79'), team) bob = self.make_participant('bob') team.set_take_for(bob, EUR('0.21'), team) charlie = self.make_participant('charlie', balance=EUR(10)) charlie.set_tip_to(team, EUR('2.00')) print("> Step 1: three weeks of donations from charlie only") print() for i in range(3): Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.79') * 3, 'bob': EUR('0.21') * 3, 'charlie': EUR('7.00'), 'team': EUR('0.00'), } assert d == expected print( "> Step 2: dan joins the party, charlie's donation is automatically " "reduced while dan catches up") print() dan = self.make_participant('dan', balance=EUR(10)) dan.set_tip_to(team, EUR('2.00')) for i in range(6): Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.79') * 9, 'bob': EUR('0.21') * 9, 'charlie': EUR('5.50'), 'dan': EUR('5.50'), 'team': EUR('0.00'), } assert d == expected print( "> Step 3: dan has caught up with charlie, they will now both give 0.50" ) print() for i in range(3): Payday.start().run(recompute_stats=0, update_cached_amounts=False) print() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.79') * 12, 'bob': EUR('0.21') * 12, 'charlie': EUR('4.00'), 'dan': EUR('4.00'), 'team': EUR('0.00'), } assert d == expected
def test_iter_payday_events(self): now = datetime.now() Payday.start().run() team = self.make_participant('team', kind='group') alice = self.make_participant('alice') self.make_exchange('mango-cc', 10000, 0, alice) self.make_exchange('mango-cc', -5000, 0, alice) self.db.run(""" UPDATE exchanges SET timestamp = "timestamp" - interval '1 week' """) bob = self.make_participant('bob') carl = self.make_participant('carl') david = self.make_participant('david') self.make_exchange('mango-cc', 10000, 0, david) david.set_tip_to(team, EUR('1.00')) team.set_take_for(bob, EUR('1.00'), team) alice.set_tip_to(bob, EUR('5.00')) assert bob.balance == 0 for i in range(2): Payday.start().run() self.db.run(""" UPDATE exchanges SET timestamp = "timestamp" - interval '1 week'; 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() # Make sure events are all in the same year delta = '%s days' % (364 - (now - datetime(now.year, 1, 1)).days) self.db.run( """ UPDATE exchanges SET timestamp = "timestamp" + interval %(delta)s; UPDATE paydays SET ts_start = ts_start + interval %(delta)s; UPDATE paydays SET ts_end = ts_end + interval %(delta)s WHERE ts_end >= ts_start; UPDATE transfers SET timestamp = "timestamp" + interval %(delta)s; """, dict(delta=delta)) events = list(iter_payday_events(self.db, bob, now.year)) assert len(events) == 9 assert events[0]['kind'] == 'totals' assert not events[0]['regular_donations']['sent'] assert events[0]['regular_donations']['received'] == EUR(12) assert events[1]['kind'] == 'day-open' assert events[1]['payday_number'] == 3 assert events[2]['balances'] == EUR(12) assert events[-1]['kind'] == 'day-close' assert events[-1]['balances'] == 0 alice = Participant.from_id(alice.id) assert alice.balance == 4990 events = list(iter_payday_events(self.db, alice, now.year)) assert events[0]['regular_donations']['sent'] == EUR(10) assert len(events) == 11 carl = Participant.from_id(carl.id) assert carl.balance == 0 events = list(iter_payday_events(self.db, carl, now.year)) assert len(events) == 0
def test_payday_start(self): payday1 = Payday.start() payday2 = Payday.start() assert payday1.__dict__ == payday2.__dict__
def test_close_page_is_not_available_during_payday(self): Payday.start() alice = self.make_participant('alice') body = self.client.GET('/alice/settings/close', auth_as=alice).text assert 'Personal Information' not in body assert 'Try Again Later' in body
def run_payday(self): Payday.start().run(recompute_stats=1)
def test_cant_post_to_close_page_during_payday(self): Payday.start() alice = self.make_participant('alice') body = self.client.POST('/alice/settings/close', auth_as=alice).text assert 'Try Again Later' in body