Repository: gratipay/gratipay.com

Issue:
turn subscriptions back on in UI
Ok, that sounds stupid but I can't find the donation widget anywhere in the site.

I am using Firefox 36.0.4 on linux. I browsed the site with the console open but did not see any js error popping up.



Discussion:
@schettino72 Thanks for writing in, sorry for the confusion. :-(

We are in the midst of [a huge transition](https://github.com/gratipay/gratipay.com/issues/3399), during which we've disabled the donation widget. Let's use this ticket for bringing it back.

+1 on https://gratipay.freshdesk.com/helpdesk/tickets/2140:

> Hey, thought you guys would like to know, I tried to click the button to give $2 a month to gratipay, but it keeps coming back with an error.

+1 https://gratipay.freshdesk.com/helpdesk/tickets/2130

+1 from @colans on #3447:

> I searched all over the Web UI, but couldn't find a way to cancel a weekly tip/payment I'm making. Is this not possible? I would expect there to be a widget on the https://gratipay.com/~$USER/giving/ page allowing me to stop a payment.

+1 @deltab at https://github.com/gratipay/gratipay.com/issues/3447#issuecomment-102573883.


Resolving patch:
diff --git a/gratipay/models/participant.py b/gratipay/models/participant.py
--- a/gratipay/models/participant.py
+++ b/gratipay/models/participant.py
@@ -372,6 +372,7 @@ class NoOneToGiveFinalGiftTo(Exception): pass
     def distribute_balance_as_final_gift(self, cursor):
         """Distribute a balance as a final gift.
         """
+        raise NotImplementedError # XXX Bring me back!
         if self.balance == 0:
             return
 
@@ -1022,60 +1023,29 @@ def update_giving_and_tippees(self):
                 Participant.from_username(tip.tippee).update_receiving(cursor)
 
     def update_giving(self, cursor=None):
+        updated = []
         # Update is_funded on tips
         if self.get_credit_card_error() == '':
             updated = (cursor or self.db).all("""
-                UPDATE current_tips
+                UPDATE current_subscriptions
                    SET is_funded = true
-                 WHERE tipper = %s
+                 WHERE subscriber = %s
                    AND is_funded IS NOT true
              RETURNING *
             """, (self.username,))
-        else:
-            tips = (cursor or self.db).all("""
-                SELECT t.*
-                  FROM current_tips t
-                  JOIN participants p2 ON p2.username = t.tippee
-                 WHERE t.tipper = %s
-                   AND t.amount > 0
-                   AND p2.is_suspicious IS NOT true
-              ORDER BY p2.claimed_time IS NULL, t.ctime ASC
-            """, (self.username,))
-            fake_balance = self.balance + self.receiving
-            updated = []
-            for tip in tips:
-                if tip.amount > fake_balance:
-                    is_funded = False
-                else:
-                    fake_balance -= tip.amount
-                    is_funded = True
-                if tip.is_funded == is_funded:
-                    continue
-                updated.append((cursor or self.db).one("""
-                    UPDATE tips
-                       SET is_funded = %s
-                     WHERE id = %s
-                 RETURNING *
-                """, (is_funded, tip.id)))
-
-        # Update giving on participant
+
         giving = (cursor or self.db).one("""
-            WITH our_tips AS (
-                     SELECT amount, p2.claimed_time
-                       FROM current_tips
-                       JOIN participants p2 ON p2.username = tippee
-                      WHERE tipper = %(username)s
-                        AND p2.is_suspicious IS NOT true
-                        AND amount > 0
-                        AND is_funded
-                 )
             UPDATE participants p
                SET giving = COALESCE((
-                       SELECT sum(amount)
-                         FROM our_tips
-                        WHERE claimed_time IS NOT NULL
+                      SELECT sum(amount)
+                        FROM current_subscriptions s
+                        JOIN teams t ON t.slug=s.team
+                       WHERE subscriber=%(username)s
+                         AND amount > 0
+                         AND is_funded
+                         AND t.is_approved
                    ), 0)
-             WHERE p.username = %(username)s
+             WHERE p.username=%(username)s
          RETURNING giving
         """, dict(username=self.username))
         self.set_attributes(giving=giving)
@@ -1341,49 +1311,43 @@ def get_tip_distribution(self):
         return tip_amounts, npatrons, contributed
 
 
-    def get_giving_for_profile(self):
-        """Given a participant id and a date, return a list and a Decimal.
-
-        This function is used to populate a participant's page for their own
-        viewing pleasure.
-
+    def get_subscriptions_for_profile(self):
+        """Return a list and a Decimal.
         """
 
-        TIPS = """\
+        SUBSCRIPTIONS = """\
 
             SELECT * FROM (
-                SELECT DISTINCT ON (tippee)
-                       amount
-                     , tippee
-                     , t.ctime
-                     , t.mtime
-                     , p.claimed_time
-                     , p.username_lower
-                     , p.number
-                  FROM tips t
-                  JOIN participants p ON p.username = t.tippee
-                 WHERE tipper = %s
-                   AND p.is_suspicious IS NOT true
-                   AND p.claimed_time IS NOT NULL
-              ORDER BY tippee
-                     , t.mtime DESC
+                SELECT DISTINCT ON (s.team)
+                       s.team   as team_slug
+                     , s.amount
+                     , s.ctime
+                     , s.mtime
+                     , t.name   as team_name
+                  FROM subscriptions s
+                  JOIN teams t ON s.team = t.slug
+                 WHERE subscriber = %s
+                   AND t.is_approved is true
+                   AND t.is_closed is not true
+              ORDER BY s.team
+                     , s.mtime DESC
             ) AS foo
             ORDER BY amount DESC
-                   , username_lower
+                   , team_slug
 
         """
-        tips = self.db.all(TIPS, (self.username,))
+        subscriptions = self.db.all(SUBSCRIPTIONS, (self.username,))
 
 
         # Compute the total.
         # ==================
 
-        total = sum([t.amount for t in tips])
+        total = sum([s.amount for s in subscriptions])
         if not total:
             # If tips is an empty list, total is int 0. We want a Decimal.
             total = Decimal('0.00')
 
-        return tips, total
+        return subscriptions, total
 
     def get_current_tips(self):
         """Get the tips this participant is currently sending to others.


Test patch:
diff --git a/tests/py/test_billing_payday.py b/tests/py/test_billing_payday.py
--- a/tests/py/test_billing_payday.py
+++ b/tests/py/test_billing_payday.py
@@ -107,6 +107,7 @@ def test_ncc_failing(self, cch, fch):
         after = self.fetch_payday()
         assert after['ncc_failing'] == 1
 
+    @pytest.mark.xfail(reason="#3399")
     def test_update_cached_amounts(self):
         team = self.make_participant('team', claimed_time='now', number='plural')
         alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
@@ -164,6 +165,7 @@ def check():
         Payday.start().update_cached_amounts()
         check()
 
+    @pytest.mark.xfail(reason="#3399")
     def test_update_cached_amounts_depth(self):
         alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
         usernames = ('bob', 'carl', 'dana', 'emma', 'fred', 'greg')
diff --git a/tests/py/test_close.py b/tests/py/test_close.py
--- a/tests/py/test_close.py
+++ b/tests/py/test_close.py
@@ -109,6 +109,7 @@ def test_wbtba_raises_NotWhitelisted_if_blacklisted(self):
 
     # dbafg - distribute_balance_as_final_gift
 
+    @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467')
     def test_dbafg_distributes_balance_as_final_gift(self):
         alice = self.make_participant('alice', claimed_time='now', balance=D('10.00'))
         bob = self.make_participant('bob', claimed_time='now')
@@ -121,6 +122,7 @@ def test_dbafg_distributes_balance_as_final_gift(self):
         assert Participant.from_username('carl').balance == D('4.00')
         assert Participant.from_username('alice').balance == D('0.00')
 
+    @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467')
     def test_dbafg_needs_claimed_tips(self):
         alice = self.make_participant('alice', claimed_time='now', balance=D('10.00'))
         bob = self.make_participant('bob')
@@ -134,6 +136,7 @@ def test_dbafg_needs_claimed_tips(self):
         assert Participant.from_username('carl').balance == D('0.00')
         assert Participant.from_username('alice').balance == D('10.00')
 
+    @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467')
     def test_dbafg_gives_all_to_claimed(self):
         alice = self.make_participant('alice', claimed_time='now', balance=D('10.00'))
         bob = self.make_participant('bob', claimed_time='now')
@@ -146,6 +149,7 @@ def test_dbafg_gives_all_to_claimed(self):
         assert Participant.from_username('carl').balance == D('0.00')
         assert Participant.from_username('alice').balance == D('0.00')
 
+    @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467')
     def test_dbafg_skips_zero_tips(self):
         alice = self.make_participant('alice', claimed_time='now', balance=D('10.00'))
         bob = self.make_participant('bob', claimed_time='now')
@@ -159,6 +163,7 @@ def test_dbafg_skips_zero_tips(self):
         assert Participant.from_username('carl').balance == D('10.00')
         assert Participant.from_username('alice').balance == D('0.00')
 
+    @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467')
     def test_dbafg_favors_highest_tippee_in_rounding_errors(self):
         alice = self.make_participant('alice', claimed_time='now', balance=D('10.00'))
         bob = self.make_participant('bob', claimed_time='now')
@@ -171,6 +176,7 @@ def test_dbafg_favors_highest_tippee_in_rounding_errors(self):
         assert Participant.from_username('carl').balance == D('6.67')
         assert Participant.from_username('alice').balance == D('0.00')
 
+    @pytest.mark.xfail(reason='https://github.com/gratipay/gratipay.com/pull/3467')
     def test_dbafg_with_zero_balance_is_a_noop(self):
         alice = self.make_participant('alice', claimed_time='now', balance=D('0.00'))
         bob = self.make_participant('bob', claimed_time='now')
diff --git a/tests/py/test_communities.py b/tests/py/test_communities.py
--- a/tests/py/test_communities.py
+++ b/tests/py/test_communities.py
@@ -1,5 +1,6 @@
 from __future__ import absolute_import, division, print_function, unicode_literals
 
+import pytest
 from gratipay.testing import Harness
 
 
@@ -19,6 +20,7 @@ def test_community_member_shows_up_on_community_listing(self):
         html = self.client.GET('/for/something/', want='response.body')
         assert html.count('alice') == 2  # entry in New Participants
 
+    @pytest.mark.xfail(reason="#3399")
     def test_givers_show_up_on_community_page(self):
 
         # Alice tips bob.
@@ -40,6 +42,7 @@ def test_givers_dont_show_up_if_they_give_zero(self):
         assert html.count('alice') == 2  # entry in New Participants only
         assert 'bob' not in html
 
+    @pytest.mark.xfail(reason="#3399")
     def test_receivers_show_up_on_community_page(self):
 
         # Bob tips alice.
diff --git a/tests/py/test_pages.py b/tests/py/test_pages.py
--- a/tests/py/test_pages.py
+++ b/tests/py/test_pages.py
@@ -162,22 +162,18 @@ def test_settings_page_available_balance(self):
         expected = "123"
         assert expected in actual
 
-    def test_giving_page(self):
+    def test_subscriptions_page(self):
+        self.make_team(is_approved=True)
         alice = self.make_participant('alice', claimed_time='now')
-        bob = self.make_participant('bob', claimed_time='now')
-        alice.set_tip_to(bob, "1.00")
-        actual = self.client.GET("/~alice/giving/", auth_as="alice").body
-        expected = "bob"
-        assert expected in actual
+        alice.set_subscription_to('TheATeam', "1.00")
+        assert "The A Team" in self.client.GET("/~alice/subscriptions/", auth_as="alice").body
 
     def test_giving_page_shows_cancelled(self):
+        self.make_team(is_approved=True)
         alice = self.make_participant('alice', claimed_time='now')
-        bob = self.make_participant('bob', claimed_time='now')
-        alice.set_tip_to(bob, "1.00")
-        alice.set_tip_to(bob, "0.00")
-        actual = self.client.GET("/~alice/giving/", auth_as="alice").body
-        assert "bob" in actual
-        assert "Cancelled" in actual
+        alice.set_subscription_to('TheATeam', "1.00")
+        alice.set_subscription_to('TheATeam', "0.00")
+        assert "Cancelled" in self.client.GET("/~alice/subscriptions/", auth_as="alice").body
 
     def test_new_participant_can_edit_profile(self):
         self.make_participant('alice', claimed_time='now')
diff --git a/tests/py/test_participant.py b/tests/py/test_participant.py
--- a/tests/py/test_participant.py
+++ b/tests/py/test_participant.py
@@ -84,6 +84,7 @@ def test_participant_can_be_instantiated(self):
         actual = Participant.from_username('alice').__class__
         assert actual is expected
 
+    @pytest.mark.xfail(reason="#3399")
     def test_bob_has_two_dollars_in_tips(self):
         expected = Decimal('2.00')
         actual = self.bob.receiving
@@ -602,38 +603,34 @@ def test_only_funded_tips_count(self):
         alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
         bob = self.make_participant('bob', claimed_time='now')
         carl = self.make_participant('carl', claimed_time='now', last_bill_result="Fail!")
-        dana = self.make_participant('dana', claimed_time='now')
-        alice.set_tip_to(dana, '3.00')
-        alice.set_tip_to(bob, '6.00')
-        bob.set_tip_to(alice, '5.00')
-        bob.set_tip_to(dana, '2.00')
-        carl.set_tip_to(dana, '2.08')
-
-        assert alice.giving == Decimal('9.00')
-        assert alice.receiving == Decimal('5.00')
-        assert bob.giving == Decimal('5.00')
-        assert bob.receiving == Decimal('6.00')
+        team = self.make_team(is_approved=True)
+
+        alice.set_subscription_to(team, '3.00') # The only funded tip
+        bob.set_subscription_to(team, '5.00')
+        carl.set_subscription_to(team, '7.00')
+
+        # TODO - Add team payroll and check receiving values
+
+        assert alice.giving == Decimal('3.00')
+        assert bob.giving == Decimal('0.00')
         assert carl.giving == Decimal('0.00')
-        assert carl.receiving == Decimal('0.00')
-        assert dana.receiving == Decimal('3.00')
-        assert dana.npatrons == 1
 
-        funded_tips = self.db.all("SELECT amount FROM tips WHERE is_funded ORDER BY id")
-        assert funded_tips == [3, 6, 5]
+        funded_tip = self.db.one("SELECT * FROM subscriptions WHERE is_funded ORDER BY id")
+        assert funded_tip.subscriber == alice.username
 
     def test_only_latest_tip_counts(self):
         alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
-        bob = self.make_participant('bob', claimed_time='now', last_bill_result='')
-        carl = self.make_participant('carl', claimed_time='now')
-        alice.set_tip_to(carl, '12.00')
-        alice.set_tip_to(carl, '3.00')
-        bob.set_tip_to(carl, '2.00')
-        bob.set_tip_to(carl, '0.00')
-        assert alice.giving == Decimal('3.00')
-        assert bob.giving == Decimal('0.00')
-        assert carl.receiving == Decimal('3.00')
-        assert carl.npatrons == 1
+        team = self.make_team(is_approved=True)
+
+        alice.set_subscription_to(team, '12.00')
+        alice.set_subscription_to(team, '4.00')
+
+        # TODO - Add team payroll and check receiving values
+
+        assert alice.giving == Decimal('4.00')
+
 
+    @pytest.mark.xfail(reason="#3399")
     def test_receiving_includes_tips_from_whitelisted_accounts(self):
         alice = self.make_participant( 'alice'
                                      , claimed_time='now'
@@ -646,6 +643,7 @@ def test_receiving_includes_tips_from_whitelisted_accounts(self):
         assert bob.receiving == Decimal('3.00')
         assert bob.npatrons == 1
 
+    @pytest.mark.xfail(reason="#3399")
     def test_receiving_includes_tips_from_unreviewed_accounts(self):
         alice = self.make_participant( 'alice'
                                      , claimed_time='now'
@@ -670,6 +668,7 @@ def test_receiving_ignores_tips_from_blacklisted_accounts(self):
         assert bob.receiving == Decimal('0.00')
         assert bob.npatrons == 0
 
+    @pytest.mark.xfail(reason="#3399")
     def test_receiving_includes_taking_when_updated_from_set_tip_to(self):
         alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
         bob = self.make_participant('bob', claimed_time='now', taking=Decimal('42.00'))
diff --git a/tests/py/test_public_json.py b/tests/py/test_public_json.py
--- a/tests/py/test_public_json.py
+++ b/tests/py/test_public_json.py
@@ -2,6 +2,7 @@
 
 import json
 
+import pytest
 from aspen.utils import utcnow
 from gratipay.testing import Harness
 
@@ -18,6 +19,7 @@ def test_on_key_gives_gratipay(self):
 
         assert data['on'] == 'gratipay'
 
+    @pytest.mark.xfail(reason="#3399")
     def test_anonymous_gets_receiving(self):
         alice = self.make_participant('alice', last_bill_result='')
         bob = self.make_participant('bob')
@@ -38,6 +40,7 @@ def test_anonymous_does_not_get_my_tip(self):
 
         assert data.has_key('my_tip') == False
 
+    @pytest.mark.xfail(reason="#3399")
     def test_anonymous_gets_giving(self):
         alice = self.make_participant('alice', last_bill_result='')
         bob = self.make_participant('bob')
@@ -70,6 +73,7 @@ def test_anonymous_gets_null_receiving_if_user_anonymous(self):
 
         assert data['receiving'] == None
 
+    @pytest.mark.xfail(reason="#3399")
     def test_authenticated_user_gets_their_tip(self):
         alice = self.make_participant('alice', last_bill_result='')
         bob = self.make_participant('bob')
@@ -83,6 +87,7 @@ def test_authenticated_user_gets_their_tip(self):
         assert data['receiving'] == '1.00'
         assert data['my_tip'] == '1.00'
 
+    @pytest.mark.xfail(reason="#3399")
     def test_authenticated_user_doesnt_get_other_peoples_tips(self):
         alice = self.make_participant('alice', last_bill_result='')
         bob = self.make_participant('bob', last_bill_result='')
@@ -100,6 +105,7 @@ def test_authenticated_user_doesnt_get_other_peoples_tips(self):
         assert data['receiving'] == '16.00'
         assert data['my_tip'] == '1.00'
 
+    @pytest.mark.xfail(reason="#3399")
     def test_authenticated_user_gets_zero_if_they_dont_tip(self):
         self.make_participant('alice', last_bill_result='')
         bob = self.make_participant('bob', last_bill_result='')
@@ -114,6 +120,7 @@ def test_authenticated_user_gets_zero_if_they_dont_tip(self):
         assert data['receiving'] == '3.00'
         assert data['my_tip'] == '0.00'
 
+    @pytest.mark.xfail(reason="#3399")
     def test_authenticated_user_gets_self_for_self(self):
         alice = self.make_participant('alice', last_bill_result='')
         bob = self.make_participant('bob')
@@ -133,6 +140,7 @@ def test_access_control_allow_origin_header_is_asterisk(self):
 
         assert response.headers['Access-Control-Allow-Origin'] == '*'
 
+    @pytest.mark.xfail(reason="#3399")
     def test_jsonp_works(self):
         alice = self.make_participant('alice', last_bill_result='')
         bob = self.make_participant('bob')
diff --git a/tests/py/test_stats.py b/tests/py/test_stats.py
--- a/tests/py/test_stats.py
+++ b/tests/py/test_stats.py
@@ -4,6 +4,7 @@
 from decimal import Decimal
 import json
 
+import pytest
 from mock import patch
 
 from gratipay import wireup
@@ -38,6 +39,7 @@ def setUp(self):
             p = self.make_participant(participant, claimed_time='now', last_bill_result='')
             setattr(self, participant, p)
 
+    @pytest.mark.xfail(reason="#3399")
     def test_get_tip_distribution_handles_a_tip(self):
         self.alice.set_tip_to(self.bob, '3.00')
         expected = ([[Decimal('3.00'), 1, Decimal('3.00'), 1.0, Decimal('1')]],
@@ -50,6 +52,7 @@ def test_get_tip_distribution_handles_no_tips(self):
         actual = self.alice.get_tip_distribution()
         assert actual == expected
 
+    @pytest.mark.xfail(reason="#3399")
     def test_get_tip_distribution_handles_multiple_tips(self):
         carl = self.make_participant('carl', claimed_time='now', last_bill_result='')
         self.alice.set_tip_to(self.bob, '1.00')
@@ -61,6 +64,7 @@ def test_get_tip_distribution_handles_multiple_tips(self):
         actual = self.bob.get_tip_distribution()
         assert actual == expected
 
+    @pytest.mark.xfail(reason="#3399")
     def test_get_tip_distribution_handles_big_tips(self):
         self.bob.update_number('plural')
         carl = self.make_participant('carl', claimed_time='now', last_bill_result='')
@@ -73,6 +77,7 @@ def test_get_tip_distribution_handles_big_tips(self):
         actual = self.bob.get_tip_distribution()
         assert actual == expected
 
+    @pytest.mark.xfail(reason="#3399")
     def test_get_tip_distribution_ignores_bad_cc(self):
         bad_cc = self.make_participant('bad_cc', claimed_time='now', last_bill_result='Failure!')
         self.alice.set_tip_to(self.bob, '1.00')
@@ -82,6 +87,7 @@ def test_get_tip_distribution_ignores_bad_cc(self):
         actual = self.bob.get_tip_distribution()
         assert actual == expected
 
+    @pytest.mark.xfail(reason="#3399")
     def test_get_tip_distribution_ignores_missing_cc(self):
         missing_cc = self.make_participant('missing_cc', claimed_time='now')
         self.alice.set_tip_to(self.bob, '1.00')
diff --git a/tests/py/test_subscription_json.py b/tests/py/test_subscription_json.py
new file mode 100644
--- /dev/null
+++ b/tests/py/test_subscription_json.py
@@ -0,0 +1,68 @@
+from __future__ import print_function, unicode_literals
+
+import json
+
+from aspen.utils import utcnow
+from gratipay.testing import Harness
+
+
+class TestTipJson(Harness):
+
+    def test_api_returns_amount_and_totals(self):
+        "Test that we get correct amounts and totals back on POSTs to subscription.json"
+
+        # First, create some test data
+        # We need accounts
+        now = utcnow()
+        self.make_team("A", is_approved=True)
+        self.make_team("B", is_approved=True)
+        self.make_participant("alice", claimed_time=now, last_bill_result='')
+
+        # Then, add a $1.50 and $3.00 subscription
+        response1 = self.client.POST( "/A/subscription.json"
+                                    , {'amount': "1.50"}
+                                    , auth_as='alice'
+                                     )
+
+        response2 = self.client.POST( "/B/subscription.json"
+                                    , {'amount': "3.00"}
+                                    , auth_as='alice'
+                                     )
+
+        # Confirm we get back the right amounts.
+        first_data = json.loads(response1.body)
+        second_data = json.loads(response2.body)
+        assert first_data['amount'] == "1.50"
+        assert first_data['total_giving'] == "1.50"
+        assert second_data['amount'] == "3.00"
+        assert second_data['total_giving'] == "4.50"
+
+
+    def test_setting_subscription_out_of_range_gets_bad_amount(self):
+        self.make_team(is_approved=True)
+        self.make_participant("alice", claimed_time='now', last_bill_result='')
+
+        response = self.client.PxST( "/TheATeam/subscription.json"
+                                   , {'amount': "1010.00"}
+                                   , auth_as='alice'
+                                    )
+        assert "bad amount" in response.body
+        assert response.code == 400
+
+        response = self.client.PxST( "/TheATeam/subscription.json"
+                                   , {'amount': "-1.00"}
+                                   , auth_as='alice'
+                                    )
+        assert "bad amount" in response.body
+        assert response.code == 400
+
+
+    def test_subscribing_to_rejected_team_fails(self):
+        self.make_team(is_approved=False)
+        self.make_participant("alice", claimed_time='now', last_bill_result='')
+        response = self.client.PxST( "/TheATeam/subscription.json"
+                                   , {'amount': "10.00"}
+                                   , auth_as='alice'
+                                    )
+        assert "unapproved team" in response.body
+        assert response.code == 400
diff --git a/tests/py/test_take.py b/tests/py/test_take.py
--- a/tests/py/test_take.py
+++ b/tests/py/test_take.py
@@ -2,6 +2,7 @@
 
 from decimal import Decimal as D
 
+import pytest
 from gratipay.testing import Harness
 from gratipay.models.participant import Participant
 
@@ -81,6 +82,7 @@ def test_if_last_week_is_less_than_a_dollar_can_increase_to_a_dollar(self):
         actual = team.set_take_for(alice, D('42.00'), team)
         assert actual == 1
 
+    @pytest.mark.xfail(reason="#3399")
     def test_get_members(self):
         team = self.make_team()
         alice = self.make_participant('alice', claimed_time='now')
@@ -92,6 +94,7 @@ def test_get_members(self):
         assert members[0]['take'] == 42
         assert members[0]['balance'] == 58
 
+    @pytest.mark.xfail(reason="#3399")
     def test_compute_actual_takes_counts_the_team_balance(self):
         team = self.make_team(balance=D('59.46'), giving=D('7.15'))
         alice = self.make_participant('alice', claimed_time='now')
@@ -106,6 +109,7 @@ def test_compute_actual_takes_counts_the_team_balance(self):
         assert takes[1]['actual_amount'] == 0
         assert takes[1]['balance'] == D('10.31')
 
+    @pytest.mark.xfail(reason="#3399")
     def test_compute_actual_takes_gives_correct_final_balance(self):
         team = self.make_team(balance=D('53.72'))
         alice = self.make_participant('alice', claimed_time='now')
@@ -120,6 +124,7 @@ def test_compute_actual_takes_gives_correct_final_balance(self):
         assert takes[1]['actual_amount'] == 14
         assert takes[1]['balance'] == D('67.72')
 
+    @pytest.mark.xfail(reason="#3399")
     def test_taking_and_receiving_are_updated_correctly(self):
         team = self.make_team()
         alice = self.make_participant('alice', claimed_time='now')
@@ -134,6 +139,7 @@ def test_taking_and_receiving_are_updated_correctly(self):
         assert alice.taking == 50
         assert alice.receiving == 60
 
+    @pytest.mark.xfail(reason="#3399")
     def test_taking_is_zero_for_team(self):
         team = self.make_team()
         alice = self.make_participant('alice', claimed_time='now')
@@ -142,6 +148,7 @@ def test_taking_is_zero_for_team(self):
         assert team.taking == 0
         assert team.receiving == 100
 
+    @pytest.mark.xfail(reason="#3399")
     def test_but_team_can_take_from_other_team(self):
         a_team = self.make_team('A Team', claimed_time='now')
         b_team = self.make_team('B Team', claimed_time='now')
@@ -152,6 +159,7 @@ def test_but_team_can_take_from_other_team(self):
         assert b_team.taking == 1
         assert b_team.receiving == 101
 
+    @pytest.mark.xfail(reason="#3399")
     def test_changes_to_team_receiving_affect_members_take(self):
         team = self.make_team()
         alice = self.make_participant('alice', claimed_time='now')
@@ -162,6 +170,7 @@ def test_changes_to_team_receiving_affect_members_take(self):
         alice = Participant.from_username('alice')
         assert alice.receiving == alice.taking == 10
 
+    @pytest.mark.xfail(reason="#3399")
     def test_changes_to_others_take_affects_members_take(self):
         team = self.make_team()
 
@@ -179,6 +188,7 @@ def test_changes_to_others_take_affects_members_take(self):
         # But get_members still uses nominal amount
         assert [m['take'] for m in  team.get_members(alice)] == [60, 42, 0]
 
+    @pytest.mark.xfail(reason="#3399")
     def test_changes_to_others_take_can_increase_members_take(self):
         team = self.make_team()
 
diff --git a/tests/py/test_tip_distribution_json.py b/tests/py/test_tip_distribution_json.py
--- a/tests/py/test_tip_distribution_json.py
+++ b/tests/py/test_tip_distribution_json.py
@@ -2,11 +2,13 @@
 
 import json
 
+import pytest
 from gratipay.testing import Harness
 
 
 class Tests(Harness):
 
+    @pytest.mark.xfail(reason="#3399")
     def test_tip_distribution_json_gives_tip_distribution(self):
         alice = self.make_participant('alice', claimed_time='now', last_bill_result='')
         bob = self.make_participant('bob', claimed_time='now', number='plural')
diff --git a/tests/py/test_tip_json.py b/tests/py/test_tip_json.py
deleted file mode 100644
--- a/tests/py/test_tip_json.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from __future__ import print_function, unicode_literals
-
-import json
-
-import pytest
-from aspen.utils import utcnow
-from gratipay.testing import Harness
-
-
-class TestTipJson(Harness):
-
-    @pytest.mark.xfail(reason="migrating to Teams; #3399")
-    def test_get_amount_and_total_back_from_api(self):
-        "Test that we get correct amounts and totals back on POSTs to tip.json"
-
-        # First, create some test data
-        # We need accounts
-        now = utcnow()
-        self.make_participant("test_tippee1", claimed_time=now)
-        self.make_participant("test_tippee2", claimed_time=now)
-        self.make_participant("test_tipper", claimed_time=now, last_bill_result='')
-
-        # Then, add a $1.50 and $3.00 tip
-        response1 = self.client.POST( "/~test_tippee1/tip.json"
-                                    , {'amount': "1.00"}
-                                    , auth_as='test_tipper'
-                                     )
-
-        response2 = self.client.POST( "/~test_tippee2/tip.json"
-                                    , {'amount': "3.00"}
-                                    , auth_as='test_tipper'
-                                     )
-
-        # Confirm we get back the right amounts.
-        first_data = json.loads(response1.body)
-        second_data = json.loads(response2.body)
-        assert first_data['amount'] == "1.00"
-        assert first_data['total_giving'] == "1.00"
-        assert second_data['amount'] == "3.00"
-        assert second_data['total_giving'] == "4.00"
-
-    @pytest.mark.xfail(reason="migrating to Teams; #3399")
-    def test_set_tip_out_of_range(self):
-        now = utcnow()
-        self.make_participant("alice", claimed_time=now)
-        self.make_participant("bob", claimed_time=now)
-
-        response = self.client.PxST( "/~alice/tip.json"
-                                   , {'amount': "110.00"}
-                                   , auth_as='bob'
-                                    )
-        assert "bad amount" in response.body
-        assert response.code == 400
-
-        response = self.client.PxST( "/~alice/tip.json"
-                                   , {'amount': "-1.00"}
-                                   , auth_as='bob'
-                                    )
-        assert "bad amount" in response.body
-        assert response.code == 400
-
-
-    @pytest.mark.xfail(reason="migrating to Teams; #3399")
-    def test_tip_to_unclaimed(self):
-        now = utcnow()
-        alice = self.make_elsewhere('twitter', 1, 'alice')
-        self.make_participant("bob", claimed_time=now)
-        response = self.client.POST( "/~%s/tip.json" % alice.participant.username
-                                   , {'amount': "10.00"}
-                                   , auth_as='bob'
-                                    )
-        data = json.loads(response.body)
-        assert response.code == 200
-        assert data['amount'] == "10.00"