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')
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')
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")
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')
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')
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)
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_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
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
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
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)
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
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
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
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
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
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
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)
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
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
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
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()
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
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()
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)
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()
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()
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()
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
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
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
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()
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
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)
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)
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
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))
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)
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
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)
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)
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))
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
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)
def test_bad_id(self): with self.assertRaises(InvalidId): Participant.from_id(1786541)
def test_null_id(self): with self.assertRaises(InvalidId): Participant.from_id(None)
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
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)
def test_good_id(self): alice = self.make_participant('alice') alice2 = Participant.from_id(alice.id) assert alice == alice2
def test_null_id(self): p = Participant.from_id(None) assert not p
def test_bad_id(self): p = Participant.from_id(1786541) assert not p
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)
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)
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
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
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