Ejemplo n.º 1
0
 def test_dbtd_gives_all_to_claimed(self):
     alice = self.make_participant('alice', balance=EUR('10.00'))
     bob = self.make_participant('bob')
     carl = self.make_stub()
     alice.set_tip_to(bob, EUR('3.00'))
     alice.set_tip_to(carl, EUR('2.00'))
     alice.distribute_balances_to_donees()
     assert Participant.from_id(bob.id).balance == EUR('10.00')
     assert Participant.from_id(carl.id).balance == EUR('0.00')
     assert Participant.from_id(alice.id).balance == EUR('0.00')
Ejemplo n.º 2
0
    def notify_participants(self):
        previous_ts_end = self.db.one("""
            SELECT ts_end
              FROM paydays
             WHERE ts_start < %s
          ORDER BY ts_end DESC
             LIMIT 1
        """, (self.ts_start,), default=constants.BIRTHDAY)

        # Income notifications
        r = self.db.all("""
            SELECT tippee, json_agg(t) AS transfers
              FROM transfers t
             WHERE "timestamp" > %s
               AND "timestamp" <= %s
          GROUP BY tippee
        """, (previous_ts_end, self.ts_end))
        for tippee_id, transfers in r:
            successes = [t for t in transfers if t['status'] == 'succeeded']
            if not successes:
                continue
            by_team = {k: sum(t['amount'] for t in v)
                       for k, v in group_by(successes, 'team').items()}
            personal = by_team.pop(None, 0)
            by_team = {Participant.from_id(k).username: v for k, v in by_team.items()}
            Participant.from_id(tippee_id).notify(
                'income',
                total=sum(t['amount'] for t in successes),
                personal=personal,
                by_team=by_team,
            )

        # Low-balance notifications
        participants = self.db.all("""
            SELECT p.*::participants
              FROM participants p
             WHERE balance < (
                     SELECT sum(amount)
                       FROM current_tips t
                       JOIN participants p2 ON p2.id = t.tippee
                      WHERE t.tipper = p.id
                        AND p2.mangopay_user_id IS NOT NULL
                        AND p2.status = 'active'
                   )
               AND EXISTS (
                     SELECT 1
                       FROM transfers t
                      WHERE t.tipper = p.id
                        AND t.timestamp > %s
                        AND t.timestamp <= %s
                        AND t.status = 'succeeded'
                   )
        """, (previous_ts_end, self.ts_end))
        for p in participants:
            p.notify('low_balance')
Ejemplo n.º 3
0
    def notify_participants(self):
        previous_ts_end = self.db.one(
            """
            SELECT ts_end
              FROM paydays
             WHERE ts_start < %s
          ORDER BY ts_end DESC
             LIMIT 1
        """,
            (self.ts_start,),
            default=constants.BIRTHDAY,
        )

        # Income notifications
        r = self.db.all(
            """
            SELECT tippee, json_agg(t) AS transfers
              FROM transfers t
             WHERE "timestamp" > %s
               AND "timestamp" <= %s
          GROUP BY tippee
        """,
            (previous_ts_end, self.ts_end),
        )
        for tippee_id, transfers in r:
            successes = [t for t in transfers if t["status"] == "succeeded"]
            if not successes:
                continue
            by_team = {k: sum(t["amount"] for t in v) for k, v in group_by(successes, "team").items()}
            personal = by_team.pop(None, 0)
            by_team = {Participant.from_id(k).username: v for k, v in by_team.items()}
            Participant.from_id(tippee_id).notify(
                "income", total=sum(t["amount"] for t in successes), personal=personal, by_team=by_team
            )

        # Low-balance notifications
        participants = self.db.all(
            """
            SELECT p.*::participants
              FROM participants p
             WHERE balance < giving
               AND giving > 0
               AND EXISTS (
                     SELECT 1
                       FROM transfers t
                      WHERE t.tipper = p.id
                        AND t.timestamp > %s
                        AND t.timestamp <= %s
                        AND t.status = 'succeeded'
                   )
        """,
            (previous_ts_end, self.ts_end),
        )
        for p in participants:
            p.notify("low_balance")
Ejemplo n.º 4
0
 def test_dbtd_needs_claimed_tips(self):
     alice = self.make_participant('alice', balance=EUR('10.00'))
     bob = self.make_stub()
     carl = self.make_stub()
     alice.set_tip_to(bob, EUR('3.00'))
     alice.set_tip_to(carl, EUR('2.00'))
     with pytest.raises(UnableToDistributeBalance):
         alice.distribute_balances_to_donees()
     assert Participant.from_id(bob.id).balance == EUR('0.00')
     assert Participant.from_id(carl.id).balance == EUR('0.00')
     assert Participant.from_id(alice.id).balance == EUR('10.00')
Ejemplo n.º 5
0
 def test_dbafg_gives_all_to_claimed(self):
     alice = self.make_participant('alice', balance=D('10.00'))
     bob = self.make_participant('bob')
     carl = self.make_stub()
     alice.set_tip_to(bob, D('3.00'))
     alice.set_tip_to(carl, D('2.00'))
     with self.db.get_cursor() as cursor:
         alice.distribute_balance_as_final_gift(cursor)
     assert Participant.from_id(bob.id).balance == D('10.00')
     assert Participant.from_id(carl.id).balance == D('0.00')
     assert Participant.from_id(alice.id).balance == D('0.00')
Ejemplo n.º 6
0
    def test_participant_can_modify_privacy_settings(self):
        # turn them all on
        self.hit_edit(data=ALL_ON)
        alice = Participant.from_id(self.alice.id)
        for k in PRIVACY_FIELDS:
            assert getattr(alice, k) in (1, 3, True)

        # turn them all off
        self.hit_edit(data=ALL_OFF)
        alice = Participant.from_id(self.alice.id)
        for k in PRIVACY_FIELDS:
            assert getattr(alice, k) in (0, 2, False)
Ejemplo n.º 7
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
Ejemplo n.º 8
0
 def test_dbafg_needs_claimed_tips(self):
     alice = self.make_participant('alice', balance=D('10.00'))
     bob = self.make_stub()
     carl = self.make_stub()
     alice.set_tip_to(bob, D('3.00'))
     alice.set_tip_to(carl, D('2.00'))
     with self.db.get_cursor() as cursor:
         with pytest.raises(alice.NoOneToGiveFinalGiftTo):
             alice.distribute_balance_as_final_gift(cursor)
     assert Participant.from_id(bob.id).balance == D('0.00')
     assert Participant.from_id(carl.id).balance == D('0.00')
     assert Participant.from_id(alice.id).balance == D('10.00')
 def test_charge_100(self):
     with self.assertRaises(Response) as cm:
         charge(self.db, self.janet, D('100'), 'http://localhost/')
     r = cm.exception
     assert r.code == 302
     janet = Participant.from_id(self.janet.id)
     assert self.janet.balance == janet.balance == 0
Ejemplo n.º 10
0
 def test_taking_is_zero_for_team(self):
     team = self.make_team()
     alice = self.make_participant('alice')
     team.add_member(alice)
     team = Participant.from_id(team.id)
     assert team.taking == 0
     assert team.receiving == 100
Ejemplo n.º 11
0
 def make_payin_and_transfer(
     self, route, tippee, amount,
     status='succeeded', error=None, payer_country=None,
     unit_amount=None, period=None
 ):
     payer = route.participant
     payin = prepare_payin(self.db, payer, amount, route)
     payin = update_payin(self.db, payin.id, 'fake', status, error)
     provider = route.network.split('-', 1)[0]
     try:
         destination = resolve_destination(
             self.db, tippee, provider, payer, payer_country, amount
         )
     except MissingPaymentAccount as e:
         destination = self.add_payment_account(e.args[0], provider)
     recipient = Participant.from_id(destination.participant)
     if tippee.kind == 'group':
         context = 'team-donation'
         team = tippee.id
     else:
         context = 'personal-donation'
         team = None
     pt = prepare_payin_transfer(
         self.db, payin, recipient, destination, context, amount,
         unit_amount, period, team
     )
     pt = update_payin_transfer(self.db, pt.id, 'fake', status, error)
     payer.update_giving()
     tippee.update_receiving()
     if team:
         recipient.update_receiving()
     return payin, pt
 def test_payout(self):
     self.make_exchange('mango-cc', 27, 0, self.homer)
     exchange = payout(self.db, self.homer, D('1.00'))
     assert exchange.note is None
     assert exchange.status == 'created'
     homer = Participant.from_id(self.homer.id)
     assert self.homer.balance == homer.balance == 26
Ejemplo n.º 13
0
def sync_with_mangopay(db):
    """We can get out of sync with MangoPay if record_exchange_result wasn't
    completed. This is where we fix that.
    """
    print("Syncing with Mangopay...")
    check_db(db)

    exchanges = db.all("""
        SELECT *, (e.timestamp < current_timestamp - interval '1 day') AS is_old
          FROM exchanges e
         WHERE e.status = 'pre'
    """)
    for e in exchanges:
        p = Participant.from_id(e.participant)
        transactions = [x for x in User(id=p.mangopay_user_id).transactions.all(
            Sort='CreationDate:DESC', Type=('PAYIN' if e.amount > 0 else 'PAYOUT')
        ) if x.Tag == str(e.id)]
        assert len(transactions) < 2
        if transactions:
            t = transactions[0]
            error = repr_error(t)
            status = t.Status.lower()
            assert (not error) ^ (status == 'failed')
            record_exchange_result(db, e.id, t.Id, status, error, p)
        elif e.is_old:
            # The exchange didn't happen, mark it as failed
            record_exchange_result(db, e.id, '', 'failed', 'interrupted', p)

    transfers = db.all("""
        SELECT *, (t.timestamp < current_timestamp - interval '1 day') AS is_old
          FROM transfers t
         WHERE t.status = 'pre'
    """)
    for t in transfers:
        tipper = Participant.from_id(t.tipper)
        transactions = [x for x in User(id=tipper.mangopay_user_id).transactions.all(
            Sort='CreationDate:DESC', Type='TRANSFER'
        ) if x.Tag == str(t.id)]
        assert len(transactions) < 2
        if transactions:
            record_transfer_result(db, t.id, transactions[0])
        elif t.is_old:
            # The transfer didn't happen, mark it as failed
            _record_transfer_result(db, t.id, 'failed', 'interrupted')

    check_db(db)
Ejemplo n.º 14
0
 def test_payout_failure(self, test_hook):
     test_hook.side_effect = Foobar
     self.make_exchange('mango-cc', 20, 0, self.homer)
     exchange = payout(self.db, self.homer, D('1.00'))
     assert exchange.status == 'failed'
     homer = Participant.from_id(self.homer.id)
     assert homer.get_bank_account_error() == exchange.note == "Foobar()"
     assert self.homer.balance == homer.balance == 20
Ejemplo n.º 15
0
 def test_charge_exception(self, test_hook):
     test_hook.side_effect = Foobar
     exchange = charge(self.db, self.janet_route, D('1.00'), 'http://localhost/')
     assert exchange.note == "Foobar()"
     assert exchange.amount
     assert exchange.status == 'failed'
     janet = Participant.from_id(self.janet.id)
     assert self.janet.balance == janet.balance == 0
 def test_charge_failure(self, test_hook):
     test_hook.side_effect = Foobar
     exchange = charge(self.db, self.janet, D('1.00'), 'http://localhost/')
     assert exchange.note == "Foobar()"
     assert exchange.amount
     assert exchange.status == 'failed'
     janet = Participant.from_id(self.janet.id)
     assert self.janet.get_credit_card_error() == 'Foobar()'
     assert self.janet.balance == janet.balance == 0
 def test_charge_success_and_wallet_creation(self):
     self.db.run("UPDATE participants SET mangopay_wallet_id = NULL")
     self.janet.set_attributes(mangopay_wallet_id=None)
     exchange = charge(self.db, self.janet, D('1.00'), 'http://localhost/')
     janet = Participant.from_id(self.janet.id)
     assert exchange.note is None
     assert exchange.amount == 10
     assert exchange.status == 'succeeded'
     assert self.janet.balance == janet.balance == 10
Ejemplo n.º 18
0
 def test_charge_100(self, save):
     def add_redirect_url_to_payin(payin):
         payin.SecureModeRedirectURL = 'some url'
         return payin
     save.side_effect = add_redirect_url_to_payin
     with self.assertRaises(Redirect):
         charge(self.db, self.janet_route, D('100'), 'http://localhost/')
     janet = Participant.from_id(self.janet.id)
     assert self.janet.balance == janet.balance == 0
Ejemplo n.º 19
0
    def test_charge_100(self, save):
        def add_redirect_url_to_payin(payin):
            payin.SecureModeRedirectURL = 'some url'
            return payin

        save.side_effect = add_redirect_url_to_payin
        with self.assertRaises(Redirect):
            charge(self.db, self.janet, D('100'), 'http://localhost/')
        janet = Participant.from_id(self.janet.id)
        assert self.janet.balance == janet.balance == 0
Ejemplo n.º 20
0
    def test_receiving_is_zero_for_patrons(self):
        alice = self.make_participant('alice', balance=EUR(100))
        bob = self.make_participant('bob')
        alice.set_tip_to(bob, EUR('3.00'))

        bob.update_goal(EUR('-1'))
        assert bob.receiving == 0
        assert bob.npatrons == 0
        alice = Participant.from_id(alice.id)
        assert alice.giving == 0
Ejemplo n.º 21
0
    def test_receiving_is_zero_for_patrons(self):
        alice = self.make_participant('alice', balance=100)
        bob = self.make_participant('bob')
        alice.set_tip_to(bob, '3.00')

        bob.update_goal(Decimal('-1'))
        assert bob.receiving == 0
        assert bob.npatrons == 0
        alice = Participant.from_id(alice.id)
        assert alice.giving == 0
Ejemplo n.º 22
0
def sync_with_mangopay(db):
    """We can get out of sync with MangoPay if record_exchange_result wasn't
    completed. This is where we fix that.
    """
    check_db(db)

    exchanges = db.all("SELECT * FROM exchanges WHERE status = 'pre'")
    for e in exchanges:
        p = Participant.from_id(e.participant)
        transactions = mangoapi.users.GetTransactions(p.mangopay_user_id)
        transactions = [x for x in transactions if x.Tag == str(e.id)]
        assert len(transactions) < 2
        if transactions:
            t = transactions[0]
            error = repr_error(t)
            status = t.Status.lower()
            assert (not error) ^ (status == 'failed')
            record_exchange_result(db, e.id, status, error, p)
        else:
            # The exchange didn't happen, remove it
            db.run("DELETE FROM exchanges WHERE id=%s", (e.id,))
            # and restore the participant's balance if it was a credit
            if e.amount < 0:
                db.run("""
                    UPDATE participants
                       SET balance=(balance + %s)
                     WHERE id=%s
                """, (-e.amount + e.fee, p.id))

    transfers = db.all("SELECT * FROM transfers WHERE status = 'pre'")
    for t in transfers:
        tipper = Participant.from_id(t.tipper)
        transactions = mangoapi.wallets.GetTransactions(tipper.mangopay_wallet_id)
        transactions = [x for x in transactions if x.Type == 'TRANSFER' and x.Tag == str(t.id)]
        assert len(transactions) < 2
        if transactions:
            record_transfer_result(db, t.id, t.tipper, t.tippee, t.amount, transactions[0])
        else:
            # The transfer didn't happen, remove it
            db.run("DELETE FROM transfers WHERE id = %s", (t.id,))

    check_db(db)
Ejemplo n.º 23
0
 def test_charge_100(self, Create):
     def add_redirect_url_to_payin(payin):
         payin.ExecutionDetails.SecureModeRedirectURL = 'some url'
         return payin
     Create.side_effect = add_redirect_url_to_payin
     with self.assertRaises(Response) as cm:
         charge(self.db, self.janet, D('100'), 'http://localhost/')
     r = cm.exception
     assert r.code == 302
     janet = Participant.from_id(self.janet.id)
     assert self.janet.balance == janet.balance == 0
Ejemplo n.º 24
0
 def test_log_in_closed_account(self):
     password = '******'
     alice = self.make_participant('alice')
     alice.update_password(password)
     alice.update_status('closed')
     r = self.log_in('alice', password)
     assert r.code == 302
     assert SESSION in r.headers.cookie
     alice2 = Participant.from_id(alice.id)
     assert alice2.status == 'active'
     assert alice2.join_time == alice.join_time
Ejemplo n.º 25
0
 def test_log_in_closed_account(self):
     password = '******'
     alice = self.make_participant('alice')
     alice.update_password(password)
     alice.update_status('closed')
     r = self.log_in('alice', password)
     assert r.code == 302
     assert SESSION in r.headers.cookie
     alice2 = Participant.from_id(alice.id)
     assert alice2.status == 'active'
     assert alice2.join_time == alice.join_time
Ejemplo n.º 26
0
 def test_charge_success_and_wallet_creation(self):
     self.db.run("DELETE FROM wallets WHERE owner = %s", (self.janet.id,))
     exchange = charge(self.db, self.janet_route, D('20'), 'http://localhost/')
     janet = Participant.from_id(self.janet.id)
     assert exchange.note is None
     assert exchange.amount == 20
     assert exchange.status == 'succeeded'
     assert self.janet.balance == janet.balance == 20
     assert janet.get_withdrawable_amount('EUR') == 20
     with mock.patch.multiple(transactions, QUARANTINE='1 month'):
         assert janet.get_withdrawable_amount('EUR') == 0
         self.db.self_check()
Ejemplo n.º 27
0
    def test_charge_100(self, Create):
        def add_redirect_url_to_payin(payin):
            payin.ExecutionDetails.SecureModeRedirectURL = 'some url'
            return payin

        Create.side_effect = add_redirect_url_to_payin
        with self.assertRaises(Response) as cm:
            charge(self.db, self.janet, D('100'), 'http://localhost/')
        r = cm.exception
        assert r.code == 302
        janet = Participant.from_id(self.janet.id)
        assert self.janet.balance == janet.balance == 0
Ejemplo n.º 28
0
 def test_charge_success_and_wallet_creation(self):
     self.db.run("DELETE FROM wallets WHERE owner = %s", (self.janet.id,))
     exchange = charge(self.db, self.janet_route, EUR('20'), 'http://localhost/')
     janet = Participant.from_id(self.janet.id)
     assert exchange.note is None
     assert exchange.amount == 20
     assert exchange.status == 'succeeded'
     assert self.janet.balance == janet.balance == 20
     assert janet.get_withdrawable_amount('EUR') == 20
     with mock.patch.multiple(transactions, QUARANTINE='1 month'):
         assert janet.get_withdrawable_amount('EUR') == 0
         self.db.self_check()
Ejemplo n.º 29
0
def sync_with_mangopay(db):
    """We can get out of sync with MangoPay if record_exchange_result wasn't
    completed. This is where we fix that.
    """
    check_db(db)

    exchanges = db.all("SELECT * FROM exchanges WHERE status = 'pre'")
    for e in exchanges:
        p = Participant.from_id(e.participant)
        transactions = Transaction.all(user_id=p.mangopay_user_id)
        transactions = [x for x in transactions if x.Tag == str(e.id)]
        assert len(transactions) < 2
        if transactions:
            t = transactions[0]
            error = repr_error(t)
            status = t.Status.lower()
            assert (not error) ^ (status == 'failed')
            record_exchange_result(db, e.id, status, error, p)
        else:
            # The exchange didn't happen
            if e.amount < 0:
                # Mark it as failed if it was a credit
                record_exchange_result(db, e.id, 'failed', 'interrupted', p)
            else:
                # Otherwise forget about it
                db.run("DELETE FROM exchanges WHERE id=%s", (e.id,))

    transfers = db.all("SELECT * FROM transfers WHERE status = 'pre'")
    for t in transfers:
        tipper = Participant.from_id(t.tipper)
        transactions = Transaction.all(user_id=tipper.mangopay_user_id)
        transactions = [x for x in transactions if x.Type == 'TRANSFER' and x.Tag == str(t.id)]
        assert len(transactions) < 2
        if transactions:
            record_transfer_result(db, t.id, transactions[0])
        else:
            # The transfer didn't happen, remove it
            db.run("DELETE FROM transfers WHERE id = %s", (t.id,))

    check_db(db)
Ejemplo n.º 30
0
 def test_charge_success_and_wallet_creation(self):
     self.db.run("UPDATE participants SET mangopay_wallet_id = NULL")
     self.janet.set_attributes(mangopay_wallet_id=None)
     exchange = charge(self.db, self.janet_route, D('20'), 'http://localhost/')
     janet = Participant.from_id(self.janet.id)
     assert exchange.note is None
     assert exchange.amount == 20
     assert exchange.status == 'succeeded'
     assert self.janet.balance == janet.balance == 20
     assert janet.withdrawable_balance == 20
     with mock.patch.multiple(transactions, QUARANTINE='1 month'):
         assert janet.withdrawable_balance == 0
         self.db.self_check()
Ejemplo n.º 31
0
 def test_payout(self):
     e = charge(self.db, self.janet, D('46.00'), 'http://localhost/')
     assert e.status == 'succeeded', e.note
     self.janet.set_tip_to(self.homer, '42.00')
     self.janet.close('downstream')
     self.homer = self.homer.refetch()
     assert self.homer.balance == 46
     exchange = payout(self.db, self.homer, D('30.00'))
     assert exchange.note is None
     assert exchange.status == 'created'
     homer = Participant.from_id(self.homer.id)
     assert self.homer.balance == homer.balance == 16
     self.db.self_check()
Ejemplo n.º 32
0
 def test_payout(self):
     e = charge(self.db, self.janet, D('46.00'), 'http://localhost/')
     assert e.status == 'succeeded', e.note
     self.janet.set_tip_to(self.homer, '42.00')
     self.janet.close('downstream')
     self.homer = self.homer.refetch()
     assert self.homer.balance == 46
     exchange = payout(self.db, self.homer, D('30.00'))
     assert exchange.note is None
     assert exchange.status == 'created'
     homer = Participant.from_id(self.homer.id)
     assert self.homer.balance == homer.balance == 16
     self.db.self_check()
Ejemplo n.º 33
0
def transfer(db, tipper, tippee, amount, context, **kw):
    tipper_wallet = NS(remote_id=kw.get('tipper_wallet_id'), remote_owner_id=kw.get('tipper_mango_id'))
    if not all(tipper_wallet.__dict__.values()):
        tipper_wallet = Participant.from_id(tipper).get_current_wallet(amount.currency)
    tippee_wallet = NS(remote_id=kw.get('tippee_wallet_id'), remote_owner_id=kw.get('tippee_mango_id'))
    if not all(tippee_wallet.__dict__.values()):
        tippee_wallet = Participant.from_id(tippee).get_current_wallet(amount.currency, create=True)
    wallet_from = tipper_wallet.remote_id
    wallet_to = tippee_wallet.remote_id
    t_id = prepare_transfer(
        db, tipper, tippee, amount, context, wallet_from, wallet_to,
        team=kw.get('team'), invoice=kw.get('invoice'), bundles=kw.get('bundles'),
    )
    tr = Transfer()
    tr.AuthorId = tipper_wallet.remote_owner_id
    tr.CreditedUserId = tippee_wallet.remote_owner_id
    tr.CreditedWalletId = wallet_to
    tr.DebitedFunds = amount.int()
    tr.DebitedWalletId = wallet_from
    tr.Fees = Money(0, amount.currency)
    tr.Tag = str(t_id)
    return execute_transfer(db, t_id, tr), t_id
Ejemplo n.º 34
0
    def test_receiving_is_zero_for_patrons(self):
        alice = self.make_participant('alice')
        alice_card = self.upsert_route(alice, 'stripe-card')
        bob = self.make_participant('bob')
        self.add_payment_account(bob, 'stripe')
        alice.set_tip_to(bob, EUR('3.00'))
        self.make_payin_and_transfer(alice_card, bob, EUR('12.00'))

        bob.update_goal(EUR('-1'))
        assert bob.receiving == 0
        assert bob.npatrons == 0
        alice = Participant.from_id(alice.id)
        assert alice.giving == 0
Ejemplo n.º 35
0
def transfer(db, tipper, tippee, amount, context, **kw):
    tipper_wallet = NS(remote_id=kw.get('tipper_wallet_id'), remote_owner_id=kw.get('tipper_mango_id'))
    if not all(tipper_wallet.__dict__.values()):
        tipper_wallet = Participant.from_id(tipper).get_current_wallet(amount.currency)
    tippee_wallet = NS(remote_id=kw.get('tippee_wallet_id'), remote_owner_id=kw.get('tippee_mango_id'))
    if not all(tippee_wallet.__dict__.values()):
        tippee_wallet = Participant.from_id(tippee).get_current_wallet(amount.currency, create=True)
    wallet_from = tipper_wallet.remote_id
    wallet_to = tippee_wallet.remote_id
    t_id = prepare_transfer(
        db, tipper, tippee, amount, context, wallet_from, wallet_to,
        team=kw.get('team'), invoice=kw.get('invoice'), bundles=kw.get('bundles'),
    )
    tr = Transfer()
    tr.AuthorId = tipper_wallet.remote_owner_id
    tr.CreditedUserId = tippee_wallet.remote_owner_id
    tr.CreditedWalletId = wallet_to
    tr.DebitedFunds = amount.int()
    tr.DebitedWalletId = wallet_from
    tr.Fees = Money(0, amount.currency)
    tr.Tag = str(t_id)
    return execute_transfer(db, t_id, tr), t_id
Ejemplo n.º 36
0
 def test_charge_success_and_wallet_creation(self):
     self.db.run("UPDATE participants SET mangopay_wallet_id = NULL")
     self.janet.set_attributes(mangopay_wallet_id=None)
     exchange = charge(self.db, self.janet, D('50'), 'http://localhost/')
     janet = Participant.from_id(self.janet.id)
     assert exchange.note is None
     assert exchange.amount == 50
     assert exchange.status == 'succeeded'
     assert self.janet.balance == janet.balance == 50
     assert janet.withdrawable_balance == 50
     with mock.patch.multiple(exchanges, QUARANTINE='1 month'):
         assert janet.withdrawable_balance == 0
         self.db.self_check()
Ejemplo n.º 37
0
    def test_receiving_is_zero_for_patrons(self):
        alice = self.make_participant('alice')
        alice_card = self.upsert_route(alice, 'stripe-card')
        bob = self.make_participant('bob')
        self.add_payment_account(bob, 'stripe')
        alice.set_tip_to(bob, EUR('3.00'))
        self.make_payin_and_transfer(alice_card, bob, EUR('12.00'))

        bob.update_goal(EUR('-1'))
        assert bob.receiving == 0
        assert bob.npatrons == 0
        alice = Participant.from_id(alice.id)
        assert alice.giving == 0
Ejemplo n.º 38
0
def recover_lost_funds(db, exchange, lost_amount, repudiation_id):
    """Recover as much money as possible from a payin which has been reverted.
    """
    original_owner = exchange.participant
    # Try (again) to swap the disputed bundles
    with db.get_cursor() as cursor:
        cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE")
        disputed_bundles = [NS(d._asdict()) for d in cursor.all("""
            SELECT *
              FROM cash_bundles
             WHERE origin = %s
               AND disputed = true
        """, (exchange.id,))]
        bundles_sum = sum(b.amount for b in disputed_bundles)
        assert bundles_sum == lost_amount - exchange.fee
        for b in disputed_bundles:
            if b.owner == original_owner:
                continue
            try_to_swap_bundle(cursor, b, original_owner)
    # Move the funds back to the original wallet
    currency = exchange.amount.currency
    chargebacks_account, credit_wallet = Participant.get_chargebacks_account(currency)
    LiberapayOrg = Participant.from_username('LiberapayOrg')
    assert LiberapayOrg
    return_payin_bundles_to_origin(db, exchange, LiberapayOrg, create_debts=True)
    # Add a debt for the fee
    create_debt(db, original_owner, LiberapayOrg.id, exchange.fee, exchange.id)
    # Send the funds to the credit wallet
    # We have to do a SettlementTransfer instead of a normal Transfer. The amount
    # can't exceed the original payin amount, so we can't settle the fee debt.
    original_owner = Participant.from_id(original_owner)
    from_wallet = original_owner.get_current_wallet(currency).remote_id
    to_wallet = credit_wallet.remote_id
    t_id = prepare_transfer(
        db, original_owner.id, chargebacks_account.id, exchange.amount, 'chargeback',
        from_wallet, to_wallet, prefer_bundles_from=exchange.id,
    )
    tr = SettlementTransfer()
    tr.AuthorId = original_owner.mangopay_user_id
    tr.CreditedUserId = chargebacks_account.mangopay_user_id
    tr.CreditedWalletId = to_wallet
    tr.DebitedFunds = Money_to_cents(exchange.amount)
    tr.DebitedWalletId = from_wallet
    tr.Fees = Money(0, currency)
    tr.RepudiationId = repudiation_id
    tr.Tag = str(t_id)
    return execute_transfer(db, t_id, tr)
Ejemplo n.º 39
0
def recover_lost_funds(db, exchange, lost_amount, repudiation_id):
    """Recover as much money as possible from a payin which has been reverted.
    """
    original_owner = exchange.participant
    # Try (again) to swap the disputed bundles
    with db.get_cursor() as cursor:
        cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE")
        disputed_bundles = [NS(d._asdict()) for d in cursor.all("""
            SELECT *
              FROM cash_bundles
             WHERE origin = %s
               AND disputed = true
        """, (exchange.id,))]
        bundles_sum = sum(b.amount for b in disputed_bundles)
        assert bundles_sum == lost_amount - exchange.fee
        for b in disputed_bundles:
            if b.owner == original_owner:
                continue
            try_to_swap_bundle(cursor, b, original_owner)
    # Move the funds back to the original wallet
    currency = exchange.amount.currency
    chargebacks_account, credit_wallet = Participant.get_chargebacks_account(currency)
    LiberapayOrg = Participant.from_username('LiberapayOrg')
    assert LiberapayOrg
    return_payin_bundles_to_origin(db, exchange, LiberapayOrg, create_debts=True)
    # Add a debt for the fee
    create_debt(db, original_owner, LiberapayOrg.id, exchange.fee, exchange.id)
    # Send the funds to the credit wallet
    # We have to do a SettlementTransfer instead of a normal Transfer. The amount
    # can't exceed the original payin amount, so we can't settle the fee debt.
    original_owner = Participant.from_id(original_owner)
    from_wallet = original_owner.get_current_wallet(currency).remote_id
    to_wallet = credit_wallet.remote_id
    t_id = prepare_transfer(
        db, original_owner.id, chargebacks_account.id, exchange.amount, 'chargeback',
        from_wallet, to_wallet, prefer_bundles_from=exchange.id,
    )
    tr = SettlementTransfer()
    tr.AuthorId = original_owner.mangopay_user_id
    tr.CreditedUserId = chargebacks_account.mangopay_user_id
    tr.CreditedWalletId = to_wallet
    tr.DebitedFunds = exchange.amount.int()
    tr.DebitedWalletId = from_wallet
    tr.Fees = Money(0, currency)
    tr.RepudiationId = repudiation_id
    tr.Tag = str(t_id)
    return execute_transfer(db, t_id, tr)
Ejemplo n.º 40
0
 def test_migrate(self):
     # Step 1
     r = self.client.POST('/migrate', initial_data)
     assert r.code == 200
     assert "Welcome, alice!" in r.text, r.text
     # Step 2
     cache_entry = resources.__cache__[abspath('www/migrate.spt')]
     simplate_context = cache_entry.resource.pages[0]
     requests = MagicMock()
     requests.post.return_value = gratipay_response
     with patch.dict(simplate_context, {'requests': requests}):
         r = self.client.PxST('/migrate?step=2', initial_data, sentry_reraise=False)
         assert r.code == 302
         assert r.headers[b'Location'] == b'?step=3'
     # Step 3
     alice = Participant.from_id(1)
     r = self.client.GET('/migrate?step=3', auth_as=alice)
     assert r.code == 200
Ejemplo n.º 41
0
def record_payout_refund(db, payout_refund):
    orig_payout = BankWirePayOut.get(payout_refund.InitialTransactionId)
    e_origin = db.one("SELECT * FROM exchanges WHERE id = %s" % (orig_payout.Tag,))
    e_refund_id = db.one("SELECT id FROM exchanges WHERE refund_ref = %s", (e_origin.id,))
    if e_refund_id:
        # Already recorded
        return e_refund_id
    amount, fee, vat = -e_origin.amount, -e_origin.fee, -e_origin.vat
    assert payout_refund.DebitedFunds == Money(int(amount * 100), 'EUR')
    assert payout_refund.Fees == Money(int(fee * 100), 'EUR')
    route = ExchangeRoute.from_id(e_origin.route)
    participant = Participant.from_id(e_origin.participant)
    return db.one("""
        INSERT INTO exchanges
               (amount, fee, vat, participant, status, route, note, refund_ref)
        VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
     RETURNING id
    """, (amount, fee, vat, participant.id, 'created', route.id, None, e_origin.id))
Ejemplo n.º 42
0
def refetch_repos():
    with website.db.get_cursor() as cursor:
        repo = cursor.one("""
            SELECT r.participant, r.platform
              FROM repositories r
             WHERE r.info_fetched_at < now() - interval '6 days'
               AND r.participant IS NOT NULL
          ORDER BY r.info_fetched_at ASC
             LIMIT 1
        """)
        if not repo:
            return
        participant = Participant.from_id(repo.participant)
        account = participant.get_account_elsewhere(repo.platform)
        sess = account.get_auth_session()
        start_time = utcnow()
        logger.debug(
            "Refetching repository data for participant ~%s from %s account %s"
            % (participant.id, account.platform, account.user_id))
        next_page = None
        for i in range(10):
            r = account.platform_data.get_repos(account,
                                                page_url=next_page,
                                                sess=sess)
            upsert_repos(cursor, r[0], participant, utcnow())
            next_page = r[2].get('next')
            if not next_page:
                break
            sleep(1)
        deleted_count = cursor.one(
            """
            WITH deleted AS (
                     DELETE FROM repositories
                      WHERE participant = %s
                        AND platform = %s
                        AND info_fetched_at < %s
                  RETURNING id
                 )
            SELECT count(*) FROM deleted
        """, (participant.id, account.platform, start_time))
        event_type = 'fetch_repos:%s' % account.id
        payload = dict(partial_list=bool(next_page),
                       deleted_count=deleted_count)
        participant.add_event(cursor, event_type, payload)
Ejemplo n.º 43
0
 def make_payin_and_transfers(
     self,
     route,
     amount,
     transfers,
     status='succeeded',
     error=None,
     payer_country=None,
     remote_id='fake',
 ):
     payer = route.participant
     payin = prepare_payin(self.db, payer, amount, route)
     payin = update_payin(self.db, payin.id, remote_id, status, error)
     provider = route.network.split('-', 1)[0]
     payin_transfers = []
     for tippee, pt_amount, opt in transfers:
         try:
             destination = resolve_destination(self.db, tippee, provider,
                                               payer, payer_country,
                                               pt_amount)
         except MissingPaymentAccount as e:
             destination = self.add_payment_account(e.args[0], provider)
         recipient = Participant.from_id(destination.participant)
         if tippee.kind == 'group':
             context = 'team-donation'
             team = tippee.id
         else:
             context = 'personal-donation'
             team = None
         pt = prepare_payin_transfer(self.db, payin, recipient, destination,
                                     context, pt_amount,
                                     opt.get('unit_amount'),
                                     opt.get('period'), team)
         pt = update_payin_transfer(self.db, pt.id,
                                    opt.get('remote_id', 'fake'),
                                    opt.get('status', status),
                                    opt.get('error', error))
         payin_transfers.append(pt)
         tippee.update_receiving()
         if team:
             recipient.update_receiving()
     payer.update_giving()
     return payin, payin_transfers
Ejemplo n.º 44
0
def transfer(db, tipper, tippee, amount, context, **kw):
    t_id = db.one("""
        INSERT INTO transfers
                    (tipper, tippee, amount, context, team, invoice, status)
             VALUES (%s, %s, %s, %s, %s, %s, 'pre')
          RETURNING id
    """, (tipper, tippee, amount, context, kw.get('team'), kw.get('invoice')))
    get = lambda id, col: db.one("SELECT {0} FROM participants WHERE id = %s".format(col), (id,))
    tr = Transfer()
    tr.AuthorId = kw.get('tipper_mango_id') or get(tipper, 'mangopay_user_id')
    tr.CreditedUserId = kw.get('tippee_mango_id') or get(tippee, 'mangopay_user_id')
    tr.CreditedWalletId = kw.get('tippee_wallet_id') or get(tippee, 'mangopay_wallet_id')
    if not tr.CreditedWalletId:
        tr.CreditedWalletId = create_wallet(db, Participant.from_id(tippee))
    tr.DebitedFunds = Money(int(amount * 100), 'EUR')
    tr.DebitedWalletId = kw.get('tipper_wallet_id') or get(tipper, 'mangopay_wallet_id')
    tr.Fees = Money(0, 'EUR')
    tr.Tag = str(t_id)
    tr.save()
    return record_transfer_result(db, t_id, tr)
Ejemplo n.º 45
0
def transfer(db, tipper, tippee, amount, context, **kw):
    t_id = db.one("""
        INSERT INTO transfers
                    (tipper, tippee, amount, context, team, status)
             VALUES (%s, %s, %s, %s, %s, 'pre')
          RETURNING id
    """, (tipper, tippee, amount, context, kw.get('team')))
    get = lambda id, col: db.one("SELECT {0} FROM participants WHERE id = %s".format(col), (id,))
    tr = Transfer()
    tr.AuthorId = kw.get('tipper_mango_id') or get(tipper, 'mangopay_user_id')
    tr.CreditedUserId = kw.get('tippee_mango_id') or get(tippee, 'mangopay_user_id')
    tr.CreditedWalletID = kw.get('tippee_wallet_id') or get(tippee, 'mangopay_wallet_id')
    if not tr.CreditedWalletID:
        tr.CreditedWalletID = create_wallet(db, Participant.from_id(tippee))
    tr.DebitedFunds = Money(int(amount * 100), 'EUR')
    tr.DebitedWalletID = kw.get('tipper_wallet_id') or get(tipper, 'mangopay_wallet_id')
    tr.Fees = Money(0, 'EUR')
    tr.Tag = str(t_id)
    tr = mangoapi.transfers.Create(tr)
    return record_transfer_result(db, t_id, tr)
Ejemplo n.º 46
0
def record_payout_refund(db, payout_refund):
    orig_payout = BankWirePayOut.get(payout_refund.InitialTransactionId)
    e_origin = db.one("SELECT * FROM exchanges WHERE id = %s", (orig_payout.Tag,))
    e_refund_id = db.one("SELECT id FROM exchanges WHERE refund_ref = %s", (e_origin.id,))
    if e_refund_id:
        # Already recorded
        return e_refund_id
    amount, fee, vat = -e_origin.amount, -e_origin.fee, -e_origin.vat
    assert payout_refund.DebitedFunds / 100 == amount
    assert payout_refund.Fees / 100 == fee
    route = ExchangeRoute.from_id(e_origin.route)
    participant = Participant.from_id(e_origin.participant)
    remote_id = payout_refund.Id
    wallet_id = e_origin.wallet_id
    return db.one("""
        INSERT INTO exchanges
               (amount, fee, vat, participant, status, route, note, refund_ref, remote_id, wallet_id)
        VALUES (%s, %s, %s, %s, 'created', %s, NULL, %s, %s, %s)
     RETURNING id
    """, (amount, fee, vat, participant.id, route.id, e_origin.id, remote_id, wallet_id))
Ejemplo n.º 47
0
 def test_migrate(self):
     # Step 1
     r = self.client.POST('/migrate', initial_data)
     assert r.code == 200
     assert "Welcome, alice!" in r.text, r.text
     # Step 2
     cache_entry = resources.__cache__[abspath('www/migrate.spt')]
     simplate_context = cache_entry.resource.pages[0]
     requests = MagicMock()
     requests.post.return_value = gratipay_response
     with patch.dict(simplate_context, {'requests': requests}):
         r = self.client.PxST('/migrate?step=2',
                              initial_data,
                              sentry_reraise=False)
         assert r.code == 302
         assert r.headers[b'Location'] == b'?step=3'
     # Step 3
     alice = Participant.from_id(1)
     r = self.client.GET('/migrate?step=3', auth_as=alice)
     assert r.code == 200
Ejemplo n.º 48
0
    def notify_participants(self):
        previous_ts_end = self.db.one("""
            SELECT ts_end
              FROM paydays
             WHERE ts_start < %s
          ORDER BY ts_end DESC
             LIMIT 1
        """, (self.ts_start, ),
                                      default=constants.BIRTHDAY)

        # Income notifications
        n = 0
        r = self.db.all(
            """
            SELECT tippee, json_agg(t) AS transfers
              FROM transfers t
             WHERE "timestamp" > %s
               AND "timestamp" <= %s
               AND context IN ('tip', 'take', 'final-gift')
               AND status = 'succeeded'
          GROUP BY tippee
        """, (previous_ts_end, self.ts_end))
        for tippee_id, transfers in r:
            p = Participant.from_id(tippee_id)
            for t in transfers:
                t['amount'] = Money(**t['amount'])
            by_team = {
                k: (MoneyBasket(t['amount']
                                for t in v), len(set(t['tipper'] for t in v)))
                for k, v in group_by(transfers, 'team').items()
            }
            total = sum((t[0] for t in by_team.values()), MoneyBasket())
            nothing = (MoneyBasket(), 0)
            personal, personal_npatrons = by_team.pop(None, nothing)
            teams = p.get_teams()
            team_ids = set(t.id for t in teams) | set(by_team.keys())
            team_names = {t.id: t.name for t in teams}
            get_username = lambda i: team_names.get(i) or self.db.one(
                "SELECT username FROM participants WHERE id = %s", (i, ))
            by_team = {
                get_username(t_id): by_team.get(t_id, nothing)
                for t_id in team_ids
            }
            p.notify(
                'income~v2',
                total=total.fuzzy_sum(p.main_currency),
                personal=personal,
                personal_npatrons=personal_npatrons,
                by_team=by_team,
                mangopay_balance=p.get_balances(),
            )
            n += 1
        log("Sent %i income notifications." % n)

        # Donation renewal notifications
        n = 0
        participants = self.db.all("""
            SELECT (SELECT p FROM participants p WHERE p.id = t.tipper) AS p
                 , json_agg((SELECT a FROM (
                       SELECT t.periodic_amount, t.tippee_username
                   ) a))
              FROM (
                     SELECT t.*, p2.username AS tippee_username
                       FROM current_tips t
                       JOIN participants p2 ON p2.id = t.tippee
                      WHERE t.amount > 0
                        AND ( t.paid_in_advance IS NULL OR
                              t.paid_in_advance < t.amount
                            )
                        AND p2.status = 'active'
                        AND p2.is_suspended IS NOT true
                        AND p2.has_payment_account
                   ) t
             WHERE EXISTS (
                     SELECT 1
                       FROM transfers tr
                      WHERE tr.tipper = t.tipper
                        AND COALESCE(tr.team, tr.tippee) = t.tippee
                        AND tr.context IN ('tip', 'take')
                        AND tr.status = 'succeeded'
                        AND tr.timestamp >= (current_timestamp - interval '9 weeks')
                   )
               AND (
                     SELECT count(*)
                       FROM notifications n
                      WHERE n.participant = t.tipper
                        AND n.event = 'donate_reminder'
                        AND n.is_new
                   ) < 2
          GROUP BY t.tipper
          ORDER BY t.tipper
        """)
        for p, donations in participants:
            for tip in donations:
                tip['periodic_amount'] = Money(**tip['periodic_amount'])
            p.notify('donate_reminder', donations=donations)
            n += 1
        log("Sent %i donate_reminder notifications." % n)
Ejemplo n.º 49
0
 def test_bad_id(self):
     with self.assertRaises(InvalidId):
         Participant.from_id(1786541)
Ejemplo n.º 50
0
 def test_null_id(self):
     with self.assertRaises(InvalidId):
         Participant.from_id(None)
Ejemplo n.º 51
0
def refund_payin(db, exchange, create_debts=False, refund_fee=False, dry_run=False):
    """Refund a specific payin.
    """
    assert exchange.status == 'succeeded' and exchange.remote_id, exchange
    e_refund = db.one("SELECT e.* FROM exchanges e WHERE e.refund_ref = %s", (exchange.id,))
    if e_refund and e_refund.status == 'succeeded':
        return 'already done', e_refund

    # Lock the bundles and try to swap them
    with db.get_cursor() as cursor:
        cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE")
        bundles = [NS(d._asdict()) for d in cursor.all("""
            UPDATE cash_bundles
               SET disputed = true
             WHERE origin = %s
         RETURNING *
        """, (exchange.id,))]
        bundles_sum = sum(b.amount for b in bundles)
        assert bundles_sum == exchange.amount
        original_owner = exchange.participant
        for b in bundles:
            if b.owner == original_owner:
                continue
            try_to_swap_bundle(cursor, b, original_owner)

    # Move the funds back to the original wallet
    LiberapayOrg = Participant.from_username('LiberapayOrg')
    assert LiberapayOrg
    return_payin_bundles_to_origin(db, exchange, LiberapayOrg, create_debts)

    # Add a debt for the fee
    if create_debts and refund_fee:
        create_debt(db, original_owner, LiberapayOrg.id, exchange.fee, exchange.id)

    # Compute and check the amount
    wallet = db.one("SELECT * FROM wallets WHERE remote_id = %s", (exchange.wallet_id,))
    if e_refund and e_refund.status == 'pre':
        amount = -e_refund.amount
    else:
        amount = min(wallet.balance, exchange.amount)
        if amount <= 0:
            return ('not enough money: wallet balance = %s' % wallet.balance), None

    # Stop here if this is a dry run
    zero = exchange.fee.zero()
    fee, vat = (exchange.fee, exchange.vat) if refund_fee else (zero, zero)
    if dry_run:
        msg = (
            '[dry run] full refund of payin #%s (liberapay id %s): amount = %s, fee = %s' %
            (exchange.remote_id, exchange.id, exchange.amount, exchange.fee)
        ) if amount + fee == exchange.amount + exchange.fee else (
            '[dry run] partial refund of payin #%s (liberapay id %s): %s of %s, fee %s of %s' %
            (exchange.remote_id, exchange.id, amount, exchange.amount, fee, exchange.fee)
        )
        return msg, None

    # Record the refund attempt
    participant = Participant.from_id(exchange.participant)
    if not (e_refund and e_refund.status == 'pre'):
        with db.get_cursor() as cursor:
            cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE")
            bundles = [NS(d._asdict()) for d in cursor.all("""
                SELECT *
                  FROM cash_bundles
                 WHERE origin = %s
                   AND wallet_id = %s
                   AND disputed = true
            """, (exchange.id, exchange.wallet_id))]
            e_refund = cursor.one("""
                INSERT INTO exchanges
                            (participant, amount, fee, vat, route, status, refund_ref, wallet_id)
                     VALUES (%s, %s, %s, %s, %s, 'pre', %s, %s)
                  RETURNING *
            """, (participant.id, -amount, -fee, -vat, exchange.route, exchange.id, exchange.wallet_id))
            propagate_exchange(cursor, participant, e_refund, None, e_refund.amount, bundles=bundles)

    # Submit the refund
    m_refund = PayInRefund(payin_id=exchange.remote_id)
    m_refund.AuthorId = wallet.remote_owner_id
    m_refund.Tag = str(e_refund.id)
    m_refund.DebitedFunds = amount.int()
    m_refund.Fees = -fee.int()
    try:
        m_refund.save()
    except Exception as e:
        error = repr_exception(e)
        e_refund = record_exchange_result(db, e_refund.id, '', 'failed', error, participant)
        return 'exception', e_refund
    e_refund = record_exchange_result(
        db, e_refund.id, m_refund.Id, m_refund.Status.lower(), repr_error(m_refund), participant
    )
    return e_refund.status, e_refund
Ejemplo n.º 52
0
def recover_lost_funds(db, exchange, lost_amount, repudiation_id):
    """Recover as much money as possible from a payin which has been reverted.
    """
    original_owner = exchange.participant
    # Try (again) to swap the disputed bundles
    with db.get_cursor() as cursor:
        cursor.run("LOCK TABLE cash_bundles IN EXCLUSIVE MODE")
        disputed_bundles = [
            NS(d._asdict()) for d in cursor.all(
                """
            SELECT *
              FROM cash_bundles
             WHERE origin = %s
               AND disputed = true
        """, (exchange.id, ))
        ]
        bundles_sum = sum(b.amount for b in disputed_bundles)
        assert bundles_sum == lost_amount - exchange.fee
        for b in disputed_bundles:
            if b.owner == original_owner:
                continue
            try_to_swap_bundle(cursor, b, original_owner)
    # Move the funds back to the original wallet
    chargebacks_account = Participant.get_chargebacks_account()
    LiberapayOrg = Participant.from_username('LiberapayOrg')
    assert LiberapayOrg
    grouped = group_by(disputed_bundles, lambda b: (b.owner, b.withdrawal))
    for (owner, withdrawal), bundles in grouped.items():
        assert owner != chargebacks_account.id
        if owner == original_owner:
            continue
        amount = sum(b.amount for b in bundles)
        if owner is None:
            bundles = None
            withdrawer = db.one(
                "SELECT participant FROM exchanges WHERE id = %s",
                (withdrawal, ))
            payer = LiberapayOrg.id
            create_debt(db, withdrawer, payer, amount, exchange.id)
            create_debt(db, original_owner, withdrawer, amount, exchange.id)
        else:
            payer = owner
            create_debt(db, original_owner, payer, amount, exchange.id)
        transfer(db,
                 payer,
                 original_owner,
                 amount,
                 'chargeback',
                 bundles=bundles)
    # Add a debt for the fee
    create_debt(db, original_owner, LiberapayOrg.id, exchange.fee, exchange.id)
    # Send the funds to the credit wallet
    # We have to do a SettlementTransfer instead of a normal Transfer. The amount
    # can't exceed the original payin amount, so we can't settle the fee debt.
    original_owner = Participant.from_id(original_owner)
    t_id = prepare_transfer(
        db,
        original_owner.id,
        chargebacks_account.id,
        exchange.amount,
        'chargeback',
        original_owner.mangopay_wallet_id,
        chargebacks_account.mangopay_wallet_id,
        prefer_bundles_from=exchange.id,
    )
    tr = SettlementTransfer()
    tr.AuthorId = original_owner.mangopay_user_id
    tr.CreditedUserId = chargebacks_account.mangopay_user_id
    tr.CreditedWalletId = chargebacks_account.mangopay_wallet_id
    tr.DebitedFunds = Money(int(exchange.amount * 100), 'EUR')
    tr.DebitedWalletId = original_owner.mangopay_wallet_id
    tr.Fees = Money(0, 'EUR')
    tr.RepudiationId = repudiation_id
    tr.Tag = str(t_id)
    tr.save()
    return record_transfer_result(db, t_id, tr)
Ejemplo n.º 53
0
 def test_good_id(self):
     alice = self.make_participant('alice')
     alice2 = Participant.from_id(alice.id)
     assert alice == alice2
Ejemplo n.º 54
0
 def test_null_id(self):
     p = Participant.from_id(None)
     assert not p
Ejemplo n.º 55
0
 def test_bad_id(self):
     p = Participant.from_id(1786541)
     assert not p
Ejemplo n.º 56
0
    def notify_participants(self):
        previous_ts_end = self.db.one("""
            SELECT ts_end
              FROM paydays
             WHERE ts_start < %s
          ORDER BY ts_end DESC
             LIMIT 1
        """, (self.ts_start,), default=constants.BIRTHDAY)

        # Income notifications
        n = 0
        get_username = lambda i: self.db.one(
            "SELECT username FROM participants WHERE id = %s", (i,)
        )
        r = self.db.all("""
            SELECT tippee, json_agg(t) AS transfers
              FROM transfers t
             WHERE "timestamp" > %s
               AND "timestamp" <= %s
               AND context IN ('tip', 'take', 'final-gift')
               AND status = 'succeeded'
          GROUP BY tippee
        """, (previous_ts_end, self.ts_end))
        for tippee_id, transfers in r:
            p = Participant.from_id(tippee_id)
            for t in transfers:
                t['amount'] = Money(**t['amount'])
            by_team = {k: MoneyBasket(t['amount'] for t in v)
                       for k, v in group_by(transfers, 'team').items()}
            total = sum(by_team.values(), MoneyBasket())
            personal = by_team.pop(None, 0)
            by_team = {get_username(k): v for k, v in by_team.items()}
            p.notify(
                'income',
                total=total.fuzzy_sum(p.main_currency),
                personal=personal,
                by_team=by_team,
                new_balance=p.get_balances(),
            )
            n += 1
        log("Sent %i income notifications." % n)

        # Identity-required notifications
        n = 0
        participants = self.db.all("""
            SELECT p
              FROM participants p
             WHERE mangopay_user_id IS NULL
               AND kind IN ('individual', 'organization')
               AND (p.goal IS NULL OR p.goal >= 0)
               AND EXISTS (
                     SELECT 1
                       FROM current_tips t
                       JOIN participants p2 ON p2.id = t.tipper
                      WHERE t.tippee = p.id
                        AND t.amount > 0
                        AND t.is_funded
                   )
               AND NOT EXISTS (
                     SELECT 1
                       FROM notifications n
                      WHERE n.participant = p.id
                        AND n.event = 'identity_required'
                        AND n.ts > (current_timestamp - interval '6 months')
                        AND (n.is_new OR n.email_sent IS TRUE)
                   )
        """)
        for p in participants:
            p.notify('identity_required', force_email=True)
            n += 1
        log("Sent %i identity_required notifications." % n)

        # Low-balance notifications
        n = 0
        participants = self.db.all("""
            SELECT p, COALESCE(w.balance, zero(needed)) AS balance, needed
              FROM (
                     SELECT t.tipper, sum(t.amount) AS needed
                       FROM current_tips t
                       JOIN participants p2 ON p2.id = t.tippee
                      WHERE (p2.mangopay_user_id IS NOT NULL OR p2.kind = 'group')
                        AND p2.status = 'active'
                        AND p2.is_suspended IS NOT true
                   GROUP BY t.tipper, t.amount::currency
                   ) a
              JOIN participants p ON p.id = a.tipper
         LEFT JOIN wallets w ON w.owner = p.id
                            AND w.balance::currency = needed::currency
                            AND w.is_current IS TRUE
             WHERE COALESCE(w.balance, zero(needed)) < needed
               AND EXISTS (
                     SELECT 1
                       FROM transfers t
                      WHERE t.tipper = p.id
                        AND t.timestamp > %s
                        AND t.timestamp <= %s
                        AND t.status = 'succeeded'
                        AND t.amount::currency = needed::currency
                   )
        """, (previous_ts_end, self.ts_end))
        for p, balance, needed in participants:
            p.notify('low_balance', low_balance=balance, needed=needed)
            n += 1
        log("Sent %i low_balance notifications." % n)
Ejemplo n.º 57
0
    def notify_participants(self):
        previous_ts_end = self.db.one("""
            SELECT ts_end
              FROM paydays
             WHERE ts_start < %s
          ORDER BY ts_end DESC
             LIMIT 1
        """, (self.ts_start, ),
                                      default=constants.BIRTHDAY)

        # Income notifications
        r = self.db.all(
            """
            SELECT tippee, json_agg(t) AS transfers
              FROM transfers t
             WHERE "timestamp" > %s
               AND "timestamp" <= %s
               AND context <> 'refund'
          GROUP BY tippee
        """, (previous_ts_end, self.ts_end))
        for tippee_id, transfers in r:
            successes = [t for t in transfers if t['status'] == 'succeeded']
            if not successes:
                continue
            by_team = {
                k: sum(t['amount'] for t in v)
                for k, v in group_by(successes, 'team').items()
            }
            personal = by_team.pop(None, 0)
            by_team = {
                Participant.from_id(k).username: v
                for k, v in by_team.items()
            }
            p = Participant.from_id(tippee_id)
            p.notify(
                'income',
                total=sum(t['amount'] for t in successes),
                personal=personal,
                by_team=by_team,
                new_balance=p.balance,
            )

        # Identity-required notifications
        participants = self.db.all("""
            SELECT p.*::participants
              FROM participants p
             WHERE mangopay_user_id IS NULL
               AND kind IN ('individual', 'organization')
               AND (p.goal IS NULL OR p.goal >= 0)
               AND EXISTS (
                     SELECT 1
                       FROM current_tips t
                       JOIN participants p2 ON p2.id = t.tipper
                      WHERE t.tippee = p.id
                        AND t.amount > 0
                        AND p2.balance > t.amount
                   )
        """)
        for p in participants:
            p.notify('identity_required', force_email=True)

        # Low-balance notifications
        participants = self.db.all(
            """
            SELECT p.*::participants
              FROM participants p
             WHERE balance < (
                     SELECT sum(amount)
                       FROM current_tips t
                       JOIN participants p2 ON p2.id = t.tippee
                      WHERE t.tipper = p.id
                        AND p2.mangopay_user_id IS NOT NULL
                        AND p2.status = 'active'
                        AND p2.is_suspended IS NOT true
                   )
               AND EXISTS (
                     SELECT 1
                       FROM transfers t
                      WHERE t.tipper = p.id
                        AND t.timestamp > %s
                        AND t.timestamp <= %s
                        AND t.status = 'succeeded'
                   )
        """, (previous_ts_end, self.ts_end))
        for p in participants:
            p.notify('low_balance', low_balance=p.balance)
Ejemplo n.º 58
0
 def test_taking_is_zero_for_team(self):
     team, alice = self.make_team_of_one()
     team.add_member(alice)
     team = Participant.from_id(team.id)
     assert team.taking == 0
     assert team.receiving == 100
Ejemplo n.º 59
0
 def make_payin_and_transfers(
     self,
     route,
     amount,
     transfers,
     status='succeeded',
     error=None,
     payer_country=None,
     fee=None,
     remote_id='fake',
 ):
     payer = route.participant
     payin = prepare_payin(self.db, payer, amount, route)
     provider = route.network.split('-', 1)[0]
     payin_transfers = []
     for tippee, pt_amount, opt in transfers:
         tip = opt.get('tip')
         if tip:
             assert tip.tipper == payer.id
             assert tip.tippee == tippee.id
         else:
             tip = self.db.one(
                 """
                 SELECT *
                   FROM current_tips
                  WHERE tipper = %s
                    AND tippee = %s
             """, (payer.id, tippee.id))
             assert tip
         for i in range(100):
             try:
                 payin_transfers.extend(
                     prepare_donation(self.db, payin, tip, tippee, provider,
                                      payer, payer_country, pt_amount))
             except MissingPaymentAccount as e:
                 if i > 95:
                     # Infinite loop?
                     raise
                 recipient = e.args[0]
                 if recipient.kind == 'group':
                     raise
                 self.add_payment_account(recipient, provider)
             else:
                 break
     payin = update_payin(self.db,
                          payin.id,
                          remote_id,
                          status,
                          error,
                          fee=fee)
     net_amount = payin.amount - (fee or 0)
     if len(payin_transfers) > 1:
         adjust_payin_transfers(self.db, payin, net_amount)
     else:
         pt = payin_transfers[0]
         pt = update_payin_transfer(self.db,
                                    pt.id,
                                    None,
                                    pt.status,
                                    None,
                                    amount=net_amount)
         assert pt.amount == net_amount
     payin_transfers = self.db.all(
         """
         SELECT *
           FROM payin_transfers
          WHERE payin = %s
       ORDER BY ctime
     """, (payin.id, ))
     fallback_remote_id = 'fake' if payin.status == 'succeeded' else None
     for tippee, pt_amount, opt in transfers:
         for i, pt in enumerate(payin_transfers):
             payin_transfers[i] = update_payin_transfer(
                 self.db, pt.id, opt.get('remote_id', fallback_remote_id),
                 opt.get('status', status), opt.get('error', error))
             if pt.team:
                 Participant.from_id(pt.recipient).update_receiving()
         tippee.update_receiving()
     payer.update_giving()
     return payin, payin_transfers
Ejemplo n.º 60
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