예제 #1
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'
예제 #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_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')
예제 #4
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
예제 #5
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()
예제 #6
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())
예제 #7
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']
예제 #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 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
예제 #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_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
예제 #15
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
 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']
예제 #17
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_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
    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
예제 #22
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
예제 #23
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
예제 #24
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
예제 #25
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
예제 #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_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
예제 #29
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
예제 #30
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'),
     }
예제 #31
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')]
예제 #32
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')],
            }
예제 #33
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
예제 #34
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',
         }
예제 #35
0
 def test_end(self):
     Payday.start().end()
     result = self.db.one("SELECT count(*) FROM paydays "
                          "WHERE ts_end > '1970-01-01'")
     assert result == 1
예제 #36
0
    def test_update_cached_amounts(self):
        team = self.make_participant('team', kind='group')
        alice = self.make_participant('alice')
        alice_card = self.upsert_route(alice, 'stripe-card')
        bob = self.make_participant('bob')
        carl = self.make_participant('carl')
        carl_card = self.upsert_route(carl, 'stripe-card')
        dana = self.make_participant('dana')
        emma = Participant.make_stub(username='******')
        team2 = self.make_participant('team2', kind='group')
        team2.add_member(dana)
        alice.set_tip_to(dana, EUR('3.00'))
        self.make_payin_and_transfer(alice_card, dana, EUR('30.00'))
        alice.set_tip_to(bob, EUR('6.00'))
        self.make_payin_and_transfer(alice_card, bob, EUR('60.00'))
        alice.set_tip_to(emma, EUR('0.50'))
        alice.set_tip_to(team, EUR('1.20'))
        alice.set_tip_to(team2, EUR('0.49'))
        self.make_payin_and_transfer(alice_card, team2, EUR('4.90'))
        bob.set_tip_to(alice, EUR('5.00'))
        team.set_take_for(bob, EUR('1.00'), team)
        self.make_payin_and_transfer(alice_card, team, EUR('12.00'))
        bob.set_tip_to(dana, EUR('2.00'))  # funded by bob's take
        bob.set_tip_to(emma, EUR('7.00'))  # not funded, insufficient receiving
        carl.set_tip_to(dana, EUR('2.08'))  # not funded, insufficient balance
        self.make_payin_and_transfer(carl_card, dana, EUR('1.56'))
        fred = self.make_participant('fred')
        fred_card = self.upsert_route(fred, 'stripe-card')
        fred.set_tip_to(dana, EUR('2.22'))
        self.make_payin_and_transfer(fred_card, dana, EUR('8.88'))
        self.db.run(
            "UPDATE participants SET is_suspended = true WHERE username = '******'"
        )
        dana.update_receiving()

        def check():
            alice = Participant.from_username('alice')
            bob = Participant.from_username('bob')
            carl = Participant.from_username('carl')
            dana = Participant.from_username('dana')
            emma = Participant.from_username('emma')
            assert alice.giving == EUR('10.69')
            assert alice.receiving == EUR('0.00')
            assert alice.npatrons == 0
            assert alice.nteampatrons == 0
            assert bob.giving == EUR('0.00')
            assert bob.taking == EUR('1.00')
            assert bob.receiving == EUR('7.00')
            assert bob.npatrons == 1
            assert bob.nteampatrons == 1
            assert carl.giving == EUR('0.00')
            assert carl.receiving == EUR('0.00')
            assert carl.npatrons == 0
            assert carl.nteampatrons == 0
            assert dana.receiving == EUR('3.49')
            assert dana.npatrons == 1
            assert dana.nteampatrons == 1
            assert emma.receiving == EUR('0.50')
            assert emma.npatrons == 1
            assert emma.nteampatrons == 0
            funded_tips = self.db.all(
                "SELECT amount FROM tips WHERE is_funded ORDER BY id")
            assert funded_tips == [
                3, 6, 0.5, EUR('1.20'),
                EUR('0.49'), EUR('2.22')
            ]

            team = Participant.from_username('team')
            assert team.receiving == EUR('1.20')
            assert team.npatrons == 1
            assert team.leftover == EUR('0.20')

            team2 = Participant.from_username('team2')
            assert team2.receiving == EUR('0.49')
            assert team2.npatrons == 1
            assert team2.leftover == EUR('0.00')

            janet = self.janet.refetch()
            assert janet.giving == 0
            assert janet.receiving == 0
            assert janet.taking == 0
            assert janet.npatrons == 0
            assert janet.nteampatrons == 0

        # Pre-test check
        check()

        # Check that update_cached_amounts doesn't mess anything up
        Payday.start().update_cached_amounts()
        check()

        # Check that update_cached_amounts actually updates amounts
        self.db.run("""
            UPDATE tips t
               SET is_funded = true
              FROM participants p
             WHERE p.id = t.tippee
               AND p.mangopay_user_id IS NOT NULL;
            UPDATE participants
               SET giving = (10000,'EUR')
                 , taking = (10000,'EUR')
             WHERE mangopay_user_id IS NOT NULL;
            UPDATE participants
               SET npatrons = 10000
                 , receiving = (10000,'EUR');
        """)
        Payday.start().update_cached_amounts()
        check()

        # Check that the update methods of Participant concur
        for p in self.db.all("SELECT p.*::participants FROM participants p"):
            p.update_receiving()
            p.update_giving()
        check()
예제 #37
0
 def test_can_post_to_close_page_during_payday(self):
     Payday.start()
     alice = self.make_participant('alice')
     response = self.client.PxST('/alice/settings/close', auth_as=alice)
     assert response.code == 302
     assert response.headers[b'Location'] == b'/alice/'
예제 #38
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='******')
        team2 = self.make_participant('team2', kind='group')
        team2.add_member(dana)
        alice.set_tip_to(dana, '3.00')
        alice.set_tip_to(bob, '6.00')
        alice.set_tip_to(emma, '0.50')
        alice.set_tip_to(team, '1.20')
        alice.set_tip_to(team2, '0.49')
        bob.set_tip_to(alice, '5.00')
        team.add_member(bob)
        team.set_take_for(bob, D('1.00'), team)
        bob.set_tip_to(dana, '2.00')  # funded by bob's take
        bob.set_tip_to(emma, '7.00')  # not funded, insufficient receiving
        carl.set_tip_to(dana, '2.08')  # not funded, insufficient balance

        def check():
            alice = Participant.from_username('alice')
            bob = Participant.from_username('bob')
            carl = Participant.from_username('carl')
            dana = Participant.from_username('dana')
            emma = Participant.from_username('emma')
            assert alice.giving == D('10.69')
            assert alice.receiving == D('5.00')
            assert bob.giving == D('7.00')
            assert bob.receiving == D('7.00')
            assert bob.taking == D('1.00')
            assert carl.giving == D('0.00')
            assert carl.receiving == D('0.00')
            assert dana.receiving == D('5.00')
            assert dana.npatrons == 2
            assert emma.receiving == D('0.50')
            assert emma.npatrons == 1
            funded_tips = self.db.all(
                "SELECT amount FROM tips WHERE is_funded ORDER BY id")
            assert funded_tips == [3, 6, 0.5, D('1.20'), D('0.49'), 5, 2]

            team = Participant.from_username('team')
            assert team.receiving == D('1.20')
            assert team.npatrons == 1

            team2 = Participant.from_username('team2')
            assert team2.receiving == D('0.49')
            assert team2.npatrons == 1

            janet = self.janet.refetch()
            assert janet.giving == 0
            assert janet.receiving == 0
            assert janet.taking == 0
            assert janet.npatrons == 0

        # Pre-test check
        check()

        # Check that update_cached_amounts doesn't mess anything up
        Payday.start().update_cached_amounts()
        check()

        # Check that update_cached_amounts actually updates amounts
        self.db.run("""
            UPDATE tips t
               SET is_funded = true
              FROM participants p
             WHERE p.id = t.tippee
               AND p.mangopay_user_id IS NOT NULL;
            UPDATE participants
               SET giving = 10000
                 , taking = 10000
             WHERE mangopay_user_id IS NOT NULL;
            UPDATE participants
               SET npatrons = 10000
                 , receiving = 10000
             WHERE status = 'active';
        """)
        Payday.start().update_cached_amounts()
        check()

        # Check that the update methods of Participant concur
        for p in self.db.all("SELECT p.*::participants FROM participants p"):
            p.update_receiving()
            p.update_giving()
        check()
예제 #39
0
 def run_payday(self):
     Payday.start().run()
예제 #40
0
    def test_wellfunded_team_with_two_early_donors(self):
        team = self.make_participant('team', kind='group')
        alice = self.make_participant('alice')
        team.set_take_for(alice, EUR('0.79'), team)
        bob = self.make_participant('bob')
        team.set_take_for(bob, EUR('0.21'), team)
        charlie = self.make_participant('charlie', balance=EUR(10))
        charlie.set_tip_to(team, EUR('1.00'))
        dan = self.make_participant('dan', balance=EUR(10))
        dan.set_tip_to(team, EUR('3.00'))

        print("> Step 1: three weeks of donations from early donors")
        print()
        for i in range(3):
            Payday.start().run(recompute_stats=0, update_cached_amounts=False)
            print()

        d = dict(self.db.all("SELECT username, balance FROM participants"))
        expected = {
            'alice': EUR('0.79') * 3,
            'bob': EUR('0.21') * 3,
            'charlie': EUR('9.25'),
            'dan': EUR('7.75'),
            'team': EUR('0.00'),
        }
        assert d == expected

        print("> Step 2: a new donor appears, the contributions of the early "
              "donors automatically decrease while the new donor catches up")
        print()
        emma = self.make_participant('emma', balance=EUR(10))
        emma.set_tip_to(team, EUR('1.00'))

        Payday.start().run(recompute_stats=0, update_cached_amounts=False)
        print()

        d = dict(self.db.all("SELECT username, balance FROM participants"))
        expected = {
            'alice': EUR('0.79') * 4,
            'bob': EUR('0.21') * 4,
            'charlie': EUR('9.19'),
            'dan': EUR('7.59'),
            'emma': EUR('9.22'),
            'team': EUR('0.00'),
        }
        assert d == expected

        Payday.start().run(recompute_stats=0, update_cached_amounts=False)
        print()

        d = dict(self.db.all("SELECT username, balance FROM participants"))
        expected = {
            'alice': EUR('0.79') * 5,
            'bob': EUR('0.21') * 5,
            'charlie': EUR('8.99'),
            'dan': EUR('7.01'),
            'emma': EUR('9.00'),
            'team': EUR('0.00'),
        }
        assert d == expected

        print("> Step 3: emma has caught up with the early donors")
        print()

        for i in range(2):
            Payday.start().run(recompute_stats=0, update_cached_amounts=False)
            print()

        d = dict(self.db.all("SELECT username, balance FROM participants"))
        expected = {
            'alice': EUR('0.79') * 7,
            'bob': EUR('0.21') * 7,
            'charlie': EUR('8.60'),
            'dan': EUR('5.80'),
            'emma': EUR('8.60'),
            'team': EUR('0.00'),
        }
        assert d == expected
예제 #41
0
 def test_payout_during_payday(self):
     self.make_exchange('mango-cc', 200, 0, self.homer)
     Payday.start()
     with self.assertRaises(PaydayIsRunning):
         payout(self.db, self.homer_route, EUR('97.35'))
예제 #42
0
    def test_payday_handles_paid_in_advance(self):
        self.make_exchange('mango-cc', EUR('2.00'), 0, self.janet)
        self.janet.set_tip_to(self.david, EUR('0.60'))
        team = self.make_participant('team', kind='group')
        team.set_take_for(self.homer, EUR('0.40'), team)
        self.janet.set_tip_to(team, EUR('0.40'))
        self.janet.distribute_balances_to_donees(final_gift=False)

        # Preliminary checks
        janet = self.janet.refetch()
        assert janet.balance == 0
        tips = self.db.all(
            """
            SELECT *
              FROM current_tips
             WHERE tipper = %s
          ORDER BY id
        """, (janet.id, ))
        assert len(tips) == 2
        assert tips[0].paid_in_advance == EUR('1.20')
        assert tips[1].paid_in_advance == EUR('0.80')
        transfers = self.db.all("SELECT * FROM transfers ORDER BY id")
        assert len(transfers) == 2
        assert transfers[0].amount == EUR('1.20')
        assert transfers[0].context == 'tip-in-advance'
        assert transfers[1].amount == EUR('0.80')
        assert transfers[1].context == 'take-in-advance'

        # Now run a payday and check the results
        Payday.start().run()
        tips = self.db.all(
            """
            SELECT *
              FROM current_tips
             WHERE tipper = %s
          ORDER BY id
        """, (janet.id, ))
        assert len(tips) == 2
        assert tips[0].paid_in_advance == EUR('0.60')
        assert tips[1].paid_in_advance == EUR('0.40')
        transfers = self.db.all("SELECT * FROM transfers ORDER BY id")
        assert len(transfers) == 4

        self.db.run(
            "UPDATE notifications SET email = true WHERE event = 'low_balance'"
        )  # temporary bypass
        emails = self.get_emails()
        assert len(emails) == 2
        assert emails[0]['to'][0] == 'david <%s>' % self.david.email
        assert '0.60' in emails[0]['subject']
        assert emails[1]['to'][0] == 'homer <%s>' % self.homer.email
        assert '0.40' in emails[1]['text']

        # Now run a second payday and check the results again
        Payday.start().run()
        tips = self.db.all(
            """
            SELECT *
              FROM current_tips
             WHERE tipper = %s
          ORDER BY id
        """, (janet.id, ))
        assert len(tips) == 2
        assert not tips[0].paid_in_advance
        assert not tips[1].paid_in_advance
        transfers = self.db.all("SELECT * FROM transfers ORDER BY id")
        assert len(transfers) == 6

        self.db.run(
            "UPDATE notifications SET email = true WHERE event = 'low_balance'"
        )  # temporary bypass
        emails = self.get_emails()
        assert len(emails) == 3
        assert emails[0]['to'][0] == 'david <%s>' % self.david.email
        assert '0.60' in emails[0]['subject']
        assert emails[1]['to'][0] == 'homer <%s>' % self.homer.email
        assert '0.40' in emails[1]['text']
        assert emails[2]['to'][0] == 'janet <%s>' % self.janet.email
        assert 'top up' in emails[2]['subject']
        assert '1.00' in emails[2]['text']
    def test_update_cached_amounts(self):
        team = self.make_participant('team', kind='group')
        alice = self.make_participant('alice', balance=100)
        bob = self.make_participant('bob')
        carl = self.make_participant('carl', balance=1.56)
        dana = self.make_participant('dana')
        emma = Participant.make_stub(username='******')
        alice.set_tip_to(dana, '3.00')
        alice.set_tip_to(bob, '6.00')
        alice.set_tip_to(emma, '0.50')
        alice.set_tip_to(team, '1.00')
        bob.set_tip_to(alice, '5.00')
        team.add_member(bob)
        team.set_take_for(bob, D('1.00'), bob)
        bob.set_tip_to(dana, '2.00')  # funded by bob's take
        bob.set_tip_to(emma, '7.00')  # not funded, insufficient receiving
        carl.set_tip_to(dana, '2.08')  # not funded, insufficient balance

        def check():
            alice = Participant.from_username('alice')
            bob = Participant.from_username('bob')
            carl = Participant.from_username('carl')
            dana = Participant.from_username('dana')
            emma = Participant.from_username('emma')
            assert alice.giving == D('10.00')
            assert alice.receiving == D('5.00')
            assert bob.giving == D('7.00')
            assert bob.receiving == D('7.00')
            assert bob.taking == D('1.00')
            assert carl.giving == D('0.00')
            assert carl.receiving == D('0.00')
            assert dana.receiving == D('5.00')
            assert dana.npatrons == 2
            assert emma.receiving == D('0.50')
            assert emma.npatrons == 1
            funded_tips = self.db.all(
                "SELECT amount FROM tips WHERE is_funded ORDER BY id")
            assert funded_tips == [3, 6, 0.5, 1, 5, 2]

        # Pre-test check
        check()

        # Check that update_cached_amounts doesn't mess anything up
        Payday.start().update_cached_amounts()
        check()

        # Check that update_cached_amounts actually updates amounts
        self.db.run("""
            UPDATE tips t
               SET is_funded = false
              FROM participants p
             WHERE p.id = t.tippee
               AND p.mangopay_user_id IS NOT NULL;
            UPDATE participants
               SET giving = 0
                 , npatrons = 0
                 , receiving = 0
                 , taking = 0
             WHERE mangopay_user_id IS NOT NULL;
        """)
        Payday.start().update_cached_amounts()
        check()
예제 #44
0
 def test_close_page_is_available_during_payday(self):
     Payday.start()
     alice = self.make_participant('alice')
     body = self.client.GET('/alice/settings/close', auth_as=alice).text
     assert '<h3>Ready?' in body
예제 #45
0
 def run_payday(self):
     self.db.run("UPDATE notifications SET ts = ts - interval '1 week'")
     payday = Payday.start()
     payday.run(recompute_stats=1)
     return payday
예제 #46
0
    def test_takes_paid_in_advance_to_now_inactive_members(self):
        team = self.make_participant('team', kind='group', accepted_currencies=None)
        alice = self.make_participant('alice', main_currency='EUR', accepted_currencies=None)
        team.set_take_for(alice, EUR('1.00'), team)
        bob = self.make_participant('bob', main_currency='USD', accepted_currencies=None)
        team.set_take_for(bob, USD('1.00'), team)

        stripe_account_alice = self.add_payment_account(
            alice, 'stripe', default_currency='EUR'
        )
        stripe_account_bob = self.add_payment_account(
            bob, 'stripe', country='US', default_currency='USD'
        )

        carl = self.make_participant('carl')
        carl.set_tip_to(team, JPY('250'))

        carl_card = ExchangeRoute.insert(
            carl, 'stripe-card', 'x', 'chargeable', remote_user_id='x'
        )
        payin, pt = self.make_payin_and_transfer(carl_card, team, JPY('1250'))
        assert pt.destination == stripe_account_alice.pk
        payin, pt = self.make_payin_and_transfer(carl_card, team, JPY('1250'))
        assert pt.destination == stripe_account_bob.pk

        team.set_take_for(alice, EUR('0.00'), team)
        team.set_take_for(bob, None, team)
        takes = dict(self.db.all("""
            SELECT DISTINCT ON (member)
                   member, paid_in_advance
              FROM takes
          ORDER BY member, mtime DESC
        """))
        assert takes == {
            alice.id: EUR('10.00'),
            bob.id: USD('12.00'),
        }

        Payday.start().run()

        transfers = self.db.all("SELECT * FROM transfers ORDER BY id")
        assert len(transfers) == 2
        assert transfers[0].virtual is True
        assert transfers[0].tipper == carl.id
        assert transfers[0].tippee == alice.id
        assert transfers[0].amount == JPY('125')
        assert transfers[1].virtual is True
        assert transfers[1].tipper == carl.id
        assert transfers[1].tippee == bob.id
        assert transfers[1].amount == JPY('125')

        takes = dict(self.db.all("""
            SELECT DISTINCT ON (member)
                   member, paid_in_advance
              FROM takes
          ORDER BY member, mtime DESC
        """))
        assert takes == {
            alice.id: EUR('9.00'),
            bob.id: USD('10.80'),
        }

        notifications = self.db.all("SELECT * FROM notifications")
        assert len(notifications) == 0

        leftovers = dict(self.db.all("SELECT username, leftover FROM participants"))
        assert leftovers == {
            'team': MoneyBasket(JPY('250.00')),
            'alice': None,
            'bob': None,
            'carl': None,
        }
예제 #47
0
    def test_payday_handles_paid_in_advance(self):
        self.add_payment_account(self.david, 'stripe')
        self.add_payment_account(self.homer, 'paypal')
        self.make_exchange('mango-cc', EUR('2.00'), 0, self.janet)
        self.janet.set_tip_to(self.david, EUR('0.60'))
        team = self.make_participant('team', kind='group')
        team.set_take_for(self.homer, EUR('0.40'), team)
        self.janet.set_tip_to(team, EUR('0.40'))
        self.janet.distribute_balances_to_donees()

        # Preliminary checks
        janet = self.janet.refetch()
        assert janet.balance == 0
        tips = self.db.all(
            """
            SELECT *
              FROM current_tips
             WHERE tipper = %s
          ORDER BY id
        """, (janet.id, ))
        assert len(tips) == 2
        assert tips[0].paid_in_advance == EUR('1.20')
        assert tips[1].paid_in_advance == EUR('0.80')
        transfers = self.db.all("SELECT * FROM transfers ORDER BY id")
        assert len(transfers) == 2
        assert transfers[0].amount == EUR('1.20')
        assert transfers[0].context == 'tip-in-advance'
        assert transfers[1].amount == EUR('0.80')
        assert transfers[1].context == 'take-in-advance'

        # Now run a payday and check the results
        self.db.run("""
            UPDATE scheduled_payins
               SET ctime = ctime - interval '12 hours'
                 , execution_date = current_date
        """)
        Payday.start().run()
        tips = self.db.all(
            """
            SELECT *
              FROM current_tips
             WHERE tipper = %s
          ORDER BY id
        """, (janet.id, ))
        assert len(tips) == 2
        assert tips[0].paid_in_advance == EUR('0.60')
        assert tips[1].paid_in_advance == EUR('0.40')
        transfers = self.db.all("SELECT * FROM transfers ORDER BY id")
        assert len(transfers) == 4

        emails = self.get_emails()
        assert len(emails) == 3
        assert emails[0]['to'][0] == 'david <%s>' % self.david.email
        assert '0.60' in emails[0]['subject']
        assert emails[1]['to'][0] == 'homer <%s>' % self.homer.email
        assert '0.40' in emails[1]['text']
        assert emails[2]['to'][0] == 'janet <%s>' % self.janet.email
        assert 'renew your donation' in emails[2]['subject']
        assert '2 donations' in emails[2]['text']

        # Now run a second payday and check the results again
        self.db.run("UPDATE notifications SET ts = ts - interval '1 week'")
        Payday.start().run()
        tips = self.db.all(
            """
            SELECT *
              FROM current_tips
             WHERE tipper = %s
          ORDER BY id
        """, (janet.id, ))
        assert len(tips) == 2
        assert not tips[0].paid_in_advance
        assert not tips[1].paid_in_advance
        transfers = self.db.all("SELECT * FROM transfers ORDER BY id")
        assert len(transfers) == 6

        emails = self.get_emails()
        assert len(emails) == 2
        assert emails[0]['to'][0] == 'david <%s>' % self.david.email
        assert '0.60' in emails[0]['subject']
        assert emails[1]['to'][0] == 'homer <%s>' % self.homer.email
        assert '0.40' in emails[1]['text']
예제 #48
0
    def test_wellfunded_team_with_early_donor(self):
        team = self.make_participant('team', kind='group')
        alice = self.make_participant('alice')
        team.set_take_for(alice, EUR('0.79'), team)
        bob = self.make_participant('bob')
        team.set_take_for(bob, EUR('0.21'), team)
        charlie = self.make_participant('charlie', balance=EUR(10))
        charlie.set_tip_to(team, EUR('2.00'))

        print("> Step 1: three weeks of donations from charlie only")
        print()
        for i in range(3):
            Payday.start().run(recompute_stats=0, update_cached_amounts=False)
            print()

        d = dict(self.db.all("SELECT username, balance FROM participants"))
        expected = {
            'alice': EUR('0.79') * 3,
            'bob': EUR('0.21') * 3,
            'charlie': EUR('7.00'),
            'team': EUR('0.00'),
        }
        assert d == expected

        print(
            "> Step 2: dan joins the party, charlie's donation is automatically "
            "reduced while dan catches up")
        print()
        dan = self.make_participant('dan', balance=EUR(10))
        dan.set_tip_to(team, EUR('2.00'))

        for i in range(6):
            Payday.start().run(recompute_stats=0, update_cached_amounts=False)
            print()

        d = dict(self.db.all("SELECT username, balance FROM participants"))
        expected = {
            'alice': EUR('0.79') * 9,
            'bob': EUR('0.21') * 9,
            'charlie': EUR('5.50'),
            'dan': EUR('5.50'),
            'team': EUR('0.00'),
        }
        assert d == expected

        print(
            "> Step 3: dan has caught up with charlie, they will now both give 0.50"
        )
        print()
        for i in range(3):
            Payday.start().run(recompute_stats=0, update_cached_amounts=False)
            print()

        d = dict(self.db.all("SELECT username, balance FROM participants"))
        expected = {
            'alice': EUR('0.79') * 12,
            'bob': EUR('0.21') * 12,
            'charlie': EUR('4.00'),
            'dan': EUR('4.00'),
            'team': EUR('0.00'),
        }
        assert d == expected
예제 #49
0
    def test_iter_payday_events(self):
        now = datetime.now()
        Payday.start().run()
        team = self.make_participant('team', kind='group')
        alice = self.make_participant('alice')
        self.make_exchange('mango-cc', 10000, 0, alice)
        self.make_exchange('mango-cc', -5000, 0, alice)
        self.db.run("""
            UPDATE exchanges
               SET timestamp = "timestamp" - interval '1 week'
        """)
        bob = self.make_participant('bob')
        carl = self.make_participant('carl')
        david = self.make_participant('david')
        self.make_exchange('mango-cc', 10000, 0, david)
        david.set_tip_to(team, EUR('1.00'))
        team.set_take_for(bob, EUR('1.00'), team)
        alice.set_tip_to(bob, EUR('5.00'))

        assert bob.balance == 0
        for i in range(2):
            Payday.start().run()
            self.db.run("""
                UPDATE exchanges
                   SET timestamp = "timestamp" - interval '1 week';
                UPDATE paydays
                   SET ts_start = ts_start - interval '1 week'
                     , ts_end = ts_end - interval '1 week';
                UPDATE transfers
                   SET timestamp = "timestamp" - interval '1 week';
            """)
        bob = Participant.from_id(bob.id)
        assert bob.balance == 12

        Payday().start()

        # Make sure events are all in the same year
        delta = '%s days' % (364 - (now - datetime(now.year, 1, 1)).days)
        self.db.run(
            """
            UPDATE exchanges
               SET timestamp = "timestamp" + interval %(delta)s;
            UPDATE paydays
               SET ts_start = ts_start + interval %(delta)s;
            UPDATE paydays
               SET ts_end = ts_end + interval %(delta)s
             WHERE ts_end >= ts_start;
            UPDATE transfers
               SET timestamp = "timestamp" + interval %(delta)s;
        """, dict(delta=delta))

        events = list(iter_payday_events(self.db, bob, now.year))
        assert len(events) == 9
        assert events[0]['kind'] == 'totals'
        assert not events[0]['regular_donations']['sent']
        assert events[0]['regular_donations']['received'] == EUR(12)
        assert events[1]['kind'] == 'day-open'
        assert events[1]['payday_number'] == 3
        assert events[2]['balances'] == EUR(12)
        assert events[-1]['kind'] == 'day-close'
        assert events[-1]['balances'] == 0

        alice = Participant.from_id(alice.id)
        assert alice.balance == 4990
        events = list(iter_payday_events(self.db, alice, now.year))
        assert events[0]['regular_donations']['sent'] == EUR(10)
        assert len(events) == 11

        carl = Participant.from_id(carl.id)
        assert carl.balance == 0
        events = list(iter_payday_events(self.db, carl, now.year))
        assert len(events) == 0
예제 #50
0
 def test_payday_start(self):
     payday1 = Payday.start()
     payday2 = Payday.start()
     assert payday1.__dict__ == payday2.__dict__
예제 #51
0
 def test_close_page_is_not_available_during_payday(self):
     Payday.start()
     alice = self.make_participant('alice')
     body = self.client.GET('/alice/settings/close', auth_as=alice).text
     assert 'Personal Information' not in body
     assert 'Try Again Later' in body
예제 #52
0
 def run_payday(self):
     Payday.start().run(recompute_stats=1)
예제 #53
0
 def test_cant_post_to_close_page_during_payday(self):
     Payday.start()
     alice = self.make_participant('alice')
     body = self.client.POST('/alice/settings/close', auth_as=alice).text
     assert 'Try Again Later' in body