def test_payin_bank_wire_callback_unexpected(self, Get): homer = self.homer cases = ( ('failed', '000001', 'FOO', 0), ('succeeded', '000000', None, 5), ('succeeded', '000000', None, 2), ) for status, result_code, error, fee in cases: status_up = status.upper() homer.set_tip_to(self.janet, EUR('1.00')) homer.close('downstream') assert homer.balance == 0 assert homer.status == 'closed' qs = "EventType=PAYIN_NORMAL_" + status_up + "&RessourceId=123456790" payin = BankWirePayIn(Id=-1) payin.Status = status_up payin.ResultCode = result_code payin.ResultMessage = error payin.AuthorId = homer.mangopay_user_id payin.PaymentType = 'BANK_WIRE' payin.DebitedFunds = Money(242, 'EUR') payin.DeclaredDebitedFunds = payin.DebitedFunds payin.DeclaredFees = Money(fee, 'EUR') payin.Fees = Money(fee, 'EUR') payin.CreditedFunds = Money(0, 'XXX') if error else Money( 242 - fee, 'EUR') payin.CreditedWalletId = self.homer_wallet_id Get.return_value = payin r = self.callback(qs) assert r.code == 200, r.text amount = EUR(242 - fee) / 100 e = self.db.one( "SELECT * FROM exchanges ORDER BY timestamp DESC lIMIT 1") assert e.status == status assert e.amount == amount assert e.fee == EUR(fee) / 100 homer = homer.refetch() if status == 'succeeded': assert homer.balance == amount assert homer.status == 'active' else: assert homer.balance == 0 assert homer.status == 'closed' emails = self.get_emails() assert len(emails) == 1 assert emails[0]['to'][0] == 'homer <%s>' % homer.email assert status[:4] in emails[0]['subject'] self.db.self_check() homer.update_status('active') # reset for next loop run
def test_dispute_callback_lost(self, save, get_payin, get_dispute): self.make_participant( 'LiberapayOrg', kind='organization', balance=EUR('100.00'), mangopay_user_id='0', mangopay_wallet_id='0', ) save.side_effect = fake_transfer e_id = self.make_exchange('mango-cc', EUR('16'), EUR('1'), self.janet) dispute = Dispute() dispute.Id = '-1' dispute.CreationDate = utcnow() dispute.DisputedFunds = Money(1700, 'EUR') dispute.DisputeType = 'CONTESTABLE' dispute.InitialTransactionType = 'PAYIN' get_dispute.return_value = dispute payin = PayIn(tag=str(e_id)) get_payin.return_value = payin # Transfer some of the money to homer self.janet.set_tip_to(self.homer, EUR('3.68')) Payday.start().run() # Withdraw some of the money self.make_exchange('mango-ba', EUR('-2.68'), 0, self.homer) # Add a bit of money that will remain undisputed, to test bundle swapping self.make_exchange('mango-cc', EUR('0.32'), 0, self.janet) self.make_exchange('mango-cc', EUR('0.55'), 0, self.homer) # Call back self.db.self_check() for status in ('CREATED', 'CLOSED'): dispute.Status = status if status == 'CLOSED': dispute.ResultCode = 'LOST' qs = "EventType=DISPUTE_" + status + "&RessourceId=123456790" r = self.callback(qs, raise_immediately=True) assert r.code == 200, r.text self.db.self_check() # Check final state balances = dict( self.db.all("SELECT username, balance FROM participants")) assert balances == { '_chargebacks_': EUR('16.00'), 'david': 0, 'homer': 0, 'janet': 0, 'LiberapayOrg': EUR('98.19'), } debts = dict(((r[0], r[1]), r[2]) for r in self.db.all(""" SELECT p_debtor.username AS debtor, p_creditor.username AS creditor, sum(d.amount) FROM debts d JOIN participants p_debtor ON p_debtor.id = d.debtor JOIN participants p_creditor ON p_creditor.id = d.creditor WHERE d.status = 'due' GROUP BY p_debtor.username, p_creditor.username """)) assert debts == { ('janet', 'LiberapayOrg'): EUR('1.00'), ('janet', 'homer'): EUR('3.36'), ('homer', 'LiberapayOrg'): EUR('1.81'), }
def test_payout_refund_callback(self, R_Get, PO_Get): homer, ba = self.homer, self.homer_route for status in ('failed', 'succeeded'): # Create the payout self.make_exchange('mango-cc', 10, 0, homer) e_id = record_exchange(self.db, ba, EUR(-9), EUR(1), EUR(0), homer, 'pre').id assert homer.balance == 0 homer.close(None) assert homer.status == 'closed' payout = BankWirePayOut(Id=-1) payout.Status = 'SUCCEEDED' payout.ResultCode = '000000' payout.AuthorId = homer.mangopay_user_id payout.Tag = str(e_id) PO_Get.return_value = payout # Create the refund status_up = status.upper() error = 'FOO' if status == 'failed' else None refund = Refund(Id=-1) refund.DebitedFunds = Money(900, 'EUR') refund.Fees = Money(-100, 'EUR') refund.Status = status_up refund.ResultCode = '000001' if error else '000000' refund.ResultMessage = error refund.RefundReason = Reason(message='BECAUSE 42') refund.AuthorId = homer.mangopay_user_id R_Get.return_value = refund # Call back qs = "EventType=PAYOUT_REFUND_" + status_up + "&RessourceId=123456790" r = self.callback(qs) assert r.code == 200, r.text homer = homer.refetch() if status == 'failed': assert homer.balance == 0 assert homer.status == 'closed' else: assert homer.balance == 10 assert homer.status == 'active' emails = self.get_emails() assert len(emails) == 1 assert emails[0]['to'][0] == 'homer <%s>' % homer.email assert 'fail' in emails[0]['subject'] assert 'BECAUSE 42' in emails[0]['text'] self.db.self_check() homer.update_status('active') # reset for next loop run
def test_payin_bank_wire_callback(self, Get): homer = self.homer route = ExchangeRoute.insert(homer, 'mango-bw', 'x', 'chargeable') cases = ( ('failed', '000001', 'FOO'), ('failed', '101109', 'The payment period has expired'), ('succeeded', '000000', None), ) for status, result_code, error in cases: status_up = status.upper() e_id = record_exchange(self.db, route, EUR(11), EUR(0), EUR(0), homer, 'pre').id assert homer.balance == 0 homer.close(None) assert homer.status == 'closed' qs = "EventType=PAYIN_NORMAL_" + status_up + "&RessourceId=123456790" payin = BankWirePayIn(Id=-1) payin.Status = status_up payin.ResultCode = result_code payin.ResultMessage = error payin.AuthorId = homer.mangopay_user_id payin.PaymentType = 'BANK_WIRE' payin.DeclaredDebitedFunds = Money(1100, 'EUR') payin.DeclaredFees = Money(0, 'EUR') payin.CreditedFunds = Money(0, 'XXX') if error else Money( 1100, 'EUR') payin.Tag = str(e_id) Get.return_value = payin r = self.callback(qs) assert r.code == 200, r.text homer = homer.refetch() if status == 'succeeded': assert homer.balance == 11 assert homer.status == 'active' else: assert homer.balance == 0 assert homer.status == 'closed' emails = self.get_emails() assert len(emails) == 1 assert emails[0]['to'][0] == 'homer <%s>' % homer.email expected = 'expired' if result_code == '101109' else status[:4] assert expected in emails[0]['subject'] self.db.self_check() homer.update_status('active') # reset for next loop run
def _test_payin_bank_wire_callback_amount_mismatch(self, Get, fee): homer = self.homer route = ExchangeRoute.insert(homer, 'mango-bw', 'x', 'chargeable') e_id = record_exchange(self.db, route, EUR(11), EUR(0), EUR(0), homer, 'pre').id assert homer.balance == 0 homer.close(None) assert homer.status == 'closed' qs = "EventType=PAYIN_NORMAL_SUCCEEDED&RessourceId=123456790" payin = BankWirePayIn(Id=-1) payin.Status = 'SUCCEEDED' payin.ResultCode = '000000' payin.ResultMessage = None payin.AuthorId = homer.mangopay_user_id payin.PaymentType = 'BANK_WIRE' payin.DeclaredDebitedFunds = Money(4500, 'EUR') payin.DeclaredFees = Money(100, 'EUR') payin.DebitedFunds = Money(302, 'EUR') payin.Fees = Money(fee, 'EUR') payin.CreditedFunds = Money(302 - fee, 'EUR') payin.Tag = str(e_id) Get.return_value = payin r = self.callback(qs) assert r.code == 200, r.text e = self.db.one("SELECT * FROM exchanges WHERE id = %s", (e_id, )) assert e.amount == payin.CreditedFunds / 100 assert e.fee == EUR(fee) / 100 assert e.vat == EUR('0.01') assert e.status == 'succeeded' homer = homer.refetch() assert homer.balance == e.amount assert homer.status == 'active' emails = self.get_emails() assert len(emails) == 1 assert emails[0]['to'][0] == 'homer <%s>' % homer.email assert 'succ' in emails[0]['subject'] self.db.self_check()
def test_dispute_callback_won(self, save, get_payin, get_dispute): self.make_participant('LiberapayOrg', kind='organization') save.side_effect = fake_transfer e_id = self.make_exchange('mango-cc', EUR('16'), EUR('1'), self.janet) dispute = Dispute() dispute.Id = '-1' dispute.CreationDate = utcnow() dispute.DisputedFunds = Money(1700, 'EUR') dispute.DisputeType = 'CONTESTABLE' dispute.InitialTransactionType = 'PAYIN' get_dispute.return_value = dispute payin = PayIn(tag=str(e_id)) get_payin.return_value = payin # Transfer some of the money to homer self.janet.set_tip_to(self.homer, EUR('3.68')) Payday.start().run() # Withdraw some of the money self.make_exchange('mango-ba', EUR('-2.68'), 0, self.homer) # Add money that will remain undisputed, to test bundle swapping self.make_exchange('mango-cc', EUR('2.69'), 0, self.janet) # Call back self.db.self_check() for status in ('CREATED', 'CLOSED'): dispute.Status = status if status == 'CLOSED': dispute.ResultCode = 'WON' qs = "EventType=DISPUTE_" + status + "&RessourceId=123456790" r = self.callback(qs) assert r.code == 200, r.text self.db.self_check() # Check final state disputed = self.db.all("SELECT * FROM cash_bundles WHERE disputed") debts = self.db.all("SELECT * FROM debts") assert not disputed assert not debts balances = dict( self.db.all("SELECT username, balance FROM participants")) assert balances == { 'david': 0, 'homer': EUR('1.00'), 'janet': EUR('15.01'), 'LiberapayOrg': 0, }
def 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 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 IN ('tip', 'take', 'final-gift') 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 p = Participant.from_id(tippee_id) for t in transfers: t['amount'] = Money(**t['amount']) t['converted_amount'] = t['amount'].convert(p.main_currency) by_team = { k: sum(t['converted_amount'] if k is None else 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.notify( 'income', total=sum(t['converted_amount'] for t in successes), personal=personal, by_team=by_team, new_balance=p.balance, ) # Identity-required notifications 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 ) """) for p in participants: p.notify('identity_required', force_email=True) # Low-balance notifications participants = self.db.all( """ SELECT p, 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 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 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' ) """, (previous_ts_end, self.ts_end)) for p, needed in participants: p.notify('low_balance', low_balance=p.balance, needed=needed)