예제 #1
0
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()
예제 #2
0
    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
예제 #3
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']
예제 #4
0
    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
예제 #5
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')
예제 #6
0
 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'
예제 #7
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
예제 #8
0
    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()
예제 #9
0
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())
예제 #10
0
    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()
예제 #11
0
 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
예제 #12
0
 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'),
     }
예제 #13
0
 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")
예제 #14
0
    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
예제 #15
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
예제 #16
0
 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')
예제 #19
0
 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
예제 #20
0
    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
예제 #21
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
예제 #22
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
예제 #23
0
    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
예제 #24
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
예제 #26
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()
예제 #27
0
    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
예제 #28
0
 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
예제 #29
0
    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
예제 #30
0
    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
예제 #31
0
 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'),
     }
예제 #32
0
 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,
     }
예제 #33
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
예제 #34
0
    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
예제 #35
0
    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
예제 #36
0
    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
예제 #37
0
 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
예제 #38
0
 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
예제 #39
0
 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] == []
예제 #40
0
 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
예제 #42
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
예제 #43
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',
                                 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')
예제 #44
0
 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']
예제 #45
0
    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
예제 #46
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
예제 #47
0
    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()
예제 #48
0
    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
예제 #49
0
 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)
예제 #50
0
 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
예제 #51
0
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()
예제 #52
0
 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']
예제 #53
0
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()
예제 #54
0
    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')]
예제 #55
0
    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')],
            }
예제 #56
0
    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
예제 #57
0
 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
예제 #58
0
 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',
         }
예제 #59
0
 def run_payday(self):
     Payday.start().run(recompute_stats=1)
예제 #60
0
 def run_payday(self):
     Payday.start().run()