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 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_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_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_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_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_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 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 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_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_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_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_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_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_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', 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_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_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_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_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_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_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_wellfunded_team_with_two_unbalanced_currencies(self): self.set_up_team_with_two_currencies() self.donor1_eur.set_tip_to(self.team, EUR('1.50')) self.donor2_usd.set_tip_to(self.team, USD('0.60')) self.donor3_eur.set_tip_to(self.team, EUR('1.50')) self.donor4_usd.set_tip_to(self.team, USD('0.60')) Payday.start().shuffle() expected = { 'alice': MoneyBasket(EUR('1.00')), 'bob': MoneyBasket(EUR('0.50'), USD('0.60')), 'donor1_eur': MoneyBasket(EUR('99.25')), 'donor2_usd': MoneyBasket(USD('99.70')), 'donor3_eur': MoneyBasket(EUR('99.25')), 'donor4_usd': MoneyBasket(USD('99.70')), } actual = self.get_balances() assert expected == actual
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_dispute_callback_won(self, save, get_payin, get_dispute): self.make_participant('LiberapayOrg', kind='organization') save.side_effect = fake_transfer e_id = self.make_exchange('mango-cc', EUR('16'), EUR('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', EUR('-2.68'), 0, self.homer) # Add money that will remain undisputed, to test bundle swapping self.make_exchange('mango-cc', EUR('2.69'), 0, self.janet) # Call back self.db.self_check() for status in ('CREATED', 'CLOSED'): dispute.Status = status if status == 'CLOSED': dispute.ResultCode = 'WON' qs = "EventType=DISPUTE_" + status + "&RessourceId=123456790" r = self.callback(qs) assert r.code == 200, r.text self.db.self_check() # Check final state disputed = self.db.all("SELECT * FROM cash_bundles WHERE disputed") debts = self.db.all("SELECT * FROM debts") assert not disputed assert not debts balances = dict( self.db.all("SELECT username, balance FROM participants")) assert balances == { 'david': 0, 'homer': EUR('1.00'), 'janet': EUR('15.01'), 'LiberapayOrg': 0, }
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 test_transfer_takes_with_two_currencies_on_both_sides(self): self.set_up_team_with_two_currencies() self.team.set_take_for(self.alice, USD('0.01'), self.alice) self.team.set_take_for(self.bob, EUR('0.01'), self.bob) self.donor1_eur.set_tip_to(self.team, EUR('0.01')) self.donor2_usd.set_tip_to(self.team, USD('0.01')) Payday.start().shuffle() expected = { 'alice': MoneyBasket(EUR('0.01')), 'bob': MoneyBasket(USD('0.01')), 'donor1_eur': MoneyBasket(EUR('99.99')), 'donor2_usd': MoneyBasket(USD('99.99')), 'donor3_eur': MoneyBasket(EUR('100')), 'donor4_usd': MoneyBasket(USD('100')), } actual = self.get_balances() assert expected == actual
def test_mutual_tipping_through_teams(self): self.clear_tables() team = self.make_participant('team', kind='group') alice = self.make_participant('alice', balance=8) alice.set_tip_to(team, EUR('2.00')) team.set_take_for(alice, D('0.25'), team) bob = self.make_participant('bob', balance=10) bob.set_tip_to(team, EUR('2.00')) team.set_take_for(bob, D('0.75'), team) Payday.start().run() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': D('7.75'), # 8 - 0.50 + 0.25 'bob': D('10.25'), # 10 - 0.25 + 0.50 'team': D('0.00'), } assert d == expected
def test_underfunded_team(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice') team.set_take_for(alice, EUR('1.00'), team) bob = self.make_participant('bob') team.set_take_for(bob, EUR('1.00'), team) charlie = self.make_participant('charlie', balance=EUR(1000)) charlie.set_tip_to(team, EUR('0.26')) Payday.start().run() d = dict(self.db.all("SELECT username, balance FROM participants")) expected = { 'alice': EUR('0.13'), 'bob': EUR('0.13'), 'charlie': EUR('999.74'), 'team': EUR('0.00'), } assert d == expected
def test_gtlwf_works_during_payday(self): team, alice = self.make_team_of_one() self.take_last_week(team, alice, EUR('20.00')) assert team.get_takes_last_week()[alice.id] == 20 self.take_last_week(team, alice, EUR('30.00')) assert team.get_takes_last_week()[alice.id] == 30 take_this_week = EUR('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=take_this_week, team=team.id)) assert team.get_takes_last_week()[alice.id] == 30
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_payday_doesnt_process_tips_when_goal_is_negative(self): self.make_exchange('mango-cc', 20, 0, self.janet) self.janet.set_tip_to(self.homer, EUR('13.00')) self.db.run("UPDATE participants SET goal = (-1,null) WHERE username='******'") 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[self.janet.id] == [EUR(20)] assert new_balances[self.homer.id] == []
def test_it_notifies_participants(self): self.make_exchange('mango-cc', 10, 0, self.janet) self.janet.set_tip_to(self.david, EUR('4.50')) self.janet.set_tip_to(self.homer, EUR('3.50')) team = self.make_participant('team', kind='group', email='*****@*****.**') self.janet.set_tip_to(team, EUR('0.25')) team.add_member(self.david) team.set_take_for(self.david, EUR('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, EUR('0.10')) Payday.start().run() david = self.david.refetch() assert david.balance == EUR('4.73') janet = self.janet.refetch() assert janet.balance == EUR('1.77') assert janet.giving == EUR('0.25') 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 '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_transfer_tips(self): self.make_exchange('mango-cc', 1, 0, self.david) self.david.set_tip_to(self.janet, D('0.51')) self.david.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_virtually(cursor) new_balances = self.get_new_balances(cursor) assert new_balances[self.david.id] == D('0.49') assert new_balances[self.janet.id] == D('0.51') assert new_balances[self.homer.id] == 0
def test_wellfunded_team_with_early_donor(self): self.clear_tables() 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_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', country='US', 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_it_notifies_participants(self): self.janet.set_tip_to(self.david, EUR('4.50')) self.janet.set_tip_to(self.homer, EUR('3.50')) team = self.make_participant('team', kind='group', email='*****@*****.**') self.janet.set_tip_to(team, EUR('0.25')) team.add_member(self.david) team.set_take_for(self.david, EUR('0.23'), team) janet_card = self.upsert_route(self.janet, 'stripe-card') self.make_payin_and_transfer(janet_card, self.david, EUR('4.50')) self.make_payin_and_transfer(janet_card, self.homer, EUR('3.50')) self.make_payin_and_transfer(janet_card, team, EUR('25.00')) self.client.PxST('/homer/emails/', auth_as=self.homer, data={'events': 'income', 'income': ''}, xhr=True) self.db.run("UPDATE scheduled_payins SET ctime = ctime - interval '12 hours'") Payday.start().run() emails = self.get_emails() assert len(emails) == 2 assert emails[0]['to'][0] == 'david <%s>' % self.david.email assert '4.73' in emails[0]['subject'] assert emails[1]['to'][0] == 'janet <%s>' % self.janet.email assert 'renew your donation' in emails[1]['subject'] assert '2 donations' in emails[1]['text']
def test_negative_paid_in_advance(self): team = self.make_participant('team', kind='group') alice = self.make_participant('alice') team.set_take_for(alice, EUR('1.00'), team) stripe_account_alice = self.add_payment_account(alice, 'stripe') donor = self.make_participant('donor') donor.set_tip_to(team, EUR('5')) donor_card = ExchangeRoute.insert( donor, 'stripe-card', 'x', 'chargeable', remote_user_id='x' ) payin, pt = self.make_payin_and_transfer(donor_card, team, EUR('10')) assert pt.destination == stripe_account_alice.pk self.db.run("UPDATE takes SET paid_in_advance = -paid_in_advance") Payday.start().run() transfers = self.db.all("SELECT * FROM transfers ORDER BY id") assert len(transfers) == 0
def test_transfer_volume(self): dana = self.make_participant('dana') dana.close() self.run_payday() payday_2 = self.run_payday() zero = {'amount': '0.00', 'currency': 'EUR'} expected = { "date": date(payday_2), "transfer_volume": {'amount': '3.00', 'currency': 'EUR'}, "nactive": '3', "nparticipants": '5', "nusers": '4', "week_payins": zero, } actual = json.loads(self.client.GET('/about/charts.json').text)[0] assert actual == expected Payday.recompute_stats() actual = json.loads(self.client.GET('/about/charts.json').text)[0] assert actual == expected
def test_update_cached_amounts_depth(self): 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_transfer_volume(self): dana = self.make_participant('dana') dana.close(None) self.run_payday() self.run_payday() expected = { "date": today(), "transfer_volume": '3.00', "nactive": '3', "nparticipants": '4', "nusers": '4', "week_deposits": '0.00', "week_withdrawals": '0.00', "xTitle": utcnow().strftime('%Y-%m-%d'), } actual = json.loads(self.client.GET('/about/charts.json').text)[0] assert actual == expected Payday.recompute_stats() actual = json.loads(self.client.GET('/about/charts.json').text)[0] assert actual == expected
def test_payday_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.check_balances(cursor)
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[self.david.id] == [EUR('0.49')] assert new_balances[self.janet.id] == [EUR('0.51')] assert new_balances[self.homer.id] == [] nulls = cursor.all("SELECT * FROM payday_tips WHERE is_funded IS NULL") assert not nulls
def main(override_payday_checks=False): from liberapay.billing.exchanges 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 today 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" # Check that today is Wednesday wd = date.today().isoweekday() assert wd == 3, "today is not Wednesday (%s != 3)" % wd # 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 test_it_notifies_participants(self): self.make_exchange('mango-cc', 10, 0, self.janet) self.janet.set_tip_to(self.david, EUR('4.50')) self.janet.set_tip_to(self.homer, EUR('3.50')) team = self.make_participant('team', kind='group', email='*****@*****.**') self.janet.set_tip_to(team, EUR('0.25')) team.add_member(self.david) team.set_take_for(self.david, EUR('0.23'), team) self.client.POST('/homer/emails/notifications.json', auth_as=self.homer, data={'fields': 'income', 'income': ''}, xhr=True) self.db.run("UPDATE participants SET payment_providers = 1") # dirty trick Payday.start().run() david = self.david.refetch() assert david.balance == EUR('4.73') janet = self.janet.refetch() assert janet.balance == EUR('1.77') assert janet.giving == EUR('0.25') emails = self.get_emails() assert len(emails) == 2 assert emails[0]['to'][0] == 'david <%s>' % self.david.email assert '4.73' in emails[0]['subject'] assert emails[1]['to'][0] == 'janet <%s>' % self.janet.email assert 'renew your donation' in emails[1]['subject'] assert '3 donations' in emails[1]['text']
def main(): from os import environ from liberapay.billing.exchanges 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 website.env.canonical_host == 'liberapay.com': log_dir = environ['OPENSHIFT_DATA_DIR'] keep_log = True else: log_dir = '' keep_log = False try: sync_with_mangopay(website.db) Payday.start().run(log_dir, keep_log) except KeyboardInterrupt: pass except: import traceback traceback.print_exc()
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 start(cls): """Try to start a new Payday. If there is a Payday that hasn't finished yet, then the UNIQUE constraint on ts_end will kick in and notify us of that. In that case we load the existing Payday and work on it some more. We use the start time of the current Payday to synchronize our work. """ try: d = cls.db.one(""" INSERT INTO paydays (id) VALUES (COALESCE(( SELECT id FROM paydays ORDER BY id DESC LIMIT 1 ), 0) + 1) RETURNING id, (ts_start AT TIME ZONE 'UTC') AS ts_start """, back_as=dict) log("Starting payday #%s." % d['id']) except IntegrityError: # Collision, we have a Payday already. d = cls.db.one(""" SELECT id, (ts_start AT TIME ZONE 'UTC') AS ts_start FROM paydays WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz """, back_as=dict) log("Picking up payday #%s." % d['id']) d['ts_start'] = d['ts_start'].replace(tzinfo=pando.utils.utc) log("Payday started at %s." % d['ts_start']) payday = Payday() payday.__dict__.update(d) return payday
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 run_payday(self): Payday.start().run(recompute_stats=1)
def run_payday(self): Payday.start().run()