Пример #1
0
 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
Пример #2
0
 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'),
     }
Пример #3
0
 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
Пример #4
0
 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
Пример #5
0
 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()
Пример #6
0
 def test_dispute_callback_won(self, save, get_payin, get_dispute):
     self.make_participant('LiberapayOrg', kind='organization')
     save.side_effect = fake_transfer
     e_id = self.make_exchange('mango-cc', EUR('16'), EUR('1'), self.janet)
     dispute = Dispute()
     dispute.Id = '-1'
     dispute.CreationDate = utcnow()
     dispute.DisputedFunds = Money(1700, 'EUR')
     dispute.DisputeType = 'CONTESTABLE'
     dispute.InitialTransactionType = 'PAYIN'
     get_dispute.return_value = dispute
     payin = PayIn(tag=str(e_id))
     get_payin.return_value = payin
     # Transfer some of the money to homer
     self.janet.set_tip_to(self.homer, EUR('3.68'))
     Payday.start().run()
     # Withdraw some of the money
     self.make_exchange('mango-ba', EUR('-2.68'), 0, self.homer)
     # Add money that will remain undisputed, to test bundle swapping
     self.make_exchange('mango-cc', EUR('2.69'), 0, self.janet)
     # Call back
     self.db.self_check()
     for status in ('CREATED', 'CLOSED'):
         dispute.Status = status
         if status == 'CLOSED':
             dispute.ResultCode = 'WON'
         qs = "EventType=DISPUTE_" + status + "&RessourceId=123456790"
         r = self.callback(qs)
         assert r.code == 200, r.text
         self.db.self_check()
     # Check final state
     disputed = self.db.all("SELECT * FROM cash_bundles WHERE disputed")
     debts = self.db.all("SELECT * FROM debts")
     assert not disputed
     assert not debts
     balances = dict(
         self.db.all("SELECT username, balance FROM participants"))
     assert balances == {
         'david': 0,
         'homer': EUR('1.00'),
         'janet': EUR('15.01'),
         'LiberapayOrg': 0,
     }
Пример #7
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)
Пример #8
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)
Пример #9
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 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)