Skip to content

Commit

Permalink
Membership change view tests
Browse files Browse the repository at this point in the history
  • Loading branch information
rebkwok committed Jul 2, 2024
1 parent 15f3568 commit 7e69105
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 29 deletions.
186 changes: 180 additions & 6 deletions booking/tests/test_membership_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ def test_membership_checkout_new_subscription_no_backdate(client, seller, config
assert resp.context_data["customer_id"] == configured_stripe_user.userprofile.stripe_customer_id


def mock_connector_class(subscription_id, invoice_secret=None, setup_secret=None, su_status="payment_method_required"):
def mock_connector_class(invoice_secret=None, setup_secret=None, su_status="payment_method_required"):
invoice = None
pending_setup_intent = None

Expand All @@ -171,24 +171,28 @@ def mock_connector_class(subscription_id, invoice_secret=None, setup_secret=None
elif setup_secret:
pending_setup_intent = Mock(id="su", client_secret=setup_secret)


class Connector(MockConnector):

def get_subscription(self, subscription_id):
super().get_subscription(subscription_id)
return MagicMock(
id=subscription_id,
latest_invoice=invoice,
pending_setup_intent=pending_setup_intent
pending_setup_intent=pending_setup_intent,
default_payment_method="p1"
)

def get_setup_intent(self, setup_intent_id):
super().get_setup_intent(setup_intent_id)
return Mock(id=setup_intent_id, status=su_status)


return Connector


@pytest.mark.freeze_time("2024-02-12")
@patch("booking.models.membership_models.StripeConnector", MockConnector)
@patch("booking.views.membership_views.StripeConnector", mock_connector_class("sub1a", invoice_secret="pi_secret"))
@patch("booking.views.membership_views.StripeConnector", mock_connector_class(invoice_secret="pi_secret"))
def test_membership_checkout_existing_subscription_with_invoice(client, seller, configured_stripe_user):
# membership checkout page, with data from membership selection page
client.force_login(configured_stripe_user)
Expand All @@ -213,7 +217,7 @@ def test_membership_checkout_existing_subscription_with_invoice(client, seller,

@pytest.mark.freeze_time("2024-02-12")
@patch("booking.models.membership_models.StripeConnector", MockConnector)
@patch("booking.views.membership_views.StripeConnector", mock_connector_class("sub1b", setup_secret="su_secret"))
@patch("booking.views.membership_views.StripeConnector", mock_connector_class(setup_secret="su_secret"))
def test_membership_checkout_existing_subscription_with_setup_intent(client, seller, configured_stripe_user):
# membership checkout page, with data from membership selection page
client.force_login(configured_stripe_user)
Expand All @@ -233,4 +237,174 @@ def test_membership_checkout_existing_subscription_with_setup_intent(client, sel
assert resp.context_data["membership"] == m1
assert resp.context_data["customer_id"] == configured_stripe_user.userprofile.stripe_customer_id
assert resp.context_data["client_secret"] == "su_secret"
assert resp.context_data["confirm_type"] == "setup"
assert resp.context_data["confirm_type"] == "setup"


# membership change
# setup 2 memberships; 1 user membership with one of the membership types

# get - form options to change to
# can only change IF the membership is active and not cancelling, i.e. it has no end date (context var)

# changes will all start from the beginning of the next month
# cancellations will start from the beginning of the next month
# if it's the user's current (uncancelled) membership, this one will be cancelled from the end of the month and
# a new one created from start of next month
# if it's a future membership (starting at beginning of next month):
# - if it's not billed yet, it'll be cancelled from the end of the month (25th) and a new one created that starts
# from the start of next month - so this one should never be invoiced.

# post
# create new sub, cancel current one; new one created with default payment method from current one
# sub starts in future -> cancel immediately
# if sub start day < 25, means sub started earlier in the month and was not backdated (if sub start day was
# >= 25 it's always backdated to 25th). Check if the billing_anchor_date, which is the actual start date of the
# subscription, is in the future
# start date

@pytest.mark.freeze_time("2024-02-12")
@patch("booking.models.membership_models.StripeConnector", MockConnector)
@patch("booking.views.membership_views.StripeConnector", MockConnector)
def test_membership_change_get(client, seller, configured_stripe_user):
client.force_login(configured_stripe_user)
m1 = baker.make(Membership, name="m1", active=True)
m2 = baker.make(Membership, name="m2", active=True)
m2.stripe_price_id = "price_2345"
m2.save()
baker.make(
UserMembership,
user=configured_stripe_user,
membership=m1,
subscription_id="sub1",
subscription_status="active",
start_date=datetime(2024, 1, 1)
)

resp = client.get(reverse("membership_change", args=("sub1",)))
assert resp.status_code == 200
assert resp.context_data["can_change"] is True
form = resp.context_data["form"]
assert list(form.fields["membership"].queryset) == [m2]


@pytest.mark.freeze_time("2024-02-12")
@patch("booking.models.membership_models.StripeConnector", MockConnector)
@patch("booking.views.membership_views.StripeConnector", MockConnector)
def test_membership_change_get_current_membership_cancelled(client, seller, configured_stripe_user):
# can't change a cancelled membership
client.force_login(configured_stripe_user)
m1 = baker.make(Membership, name="m1", active=True)
m2 = baker.make(Membership, name="m2", active=True)
m2.stripe_price_id = "price_2345"
m2.save()
baker.make(
UserMembership,
user=configured_stripe_user,
membership=m1,
subscription_id="sub1",
subscription_status="active",
start_date=datetime(2023, 12, 1),
end_date=datetime(2024, 3, 1)
)

resp = client.get(reverse("membership_change", args=("sub1",)))
assert resp.status_code == 200
assert resp.context_data["can_change"] is False
assert resp.context_data["form"] is None


@pytest.mark.freeze_time("2024-02-12")
@patch("booking.models.membership_models.StripeConnector", MockConnector)
@patch("booking.views.membership_views.StripeConnector")
def test_membership_change_post_active_subscription(mock_conn, client, seller, configured_stripe_user):
mock_connector = MockConnector()
mock_conn.return_value = mock_connector

client.force_login(configured_stripe_user)
m1 = baker.make(Membership, name="m1", active=True)
m2 = baker.make(Membership, name="m2", active=True)
m2.stripe_price_id = "price_2345"
m2.save()
baker.make(
UserMembership,
user=configured_stripe_user,
membership=m1,
subscription_id="sub1",
subscription_status="active",
subscription_start_date=datetime(2023, 12, 25),
start_date=datetime(2024, 1, 1)
)

resp = client.post(reverse("membership_change", args=("sub1",)), {"membership": m2.id})
assert resp.status_code == 302

create_calls = mock_connector.method_calls["create_subscription"]
cancel_calls = mock_connector.method_calls["cancel_subscription"]
for calls in [create_calls, cancel_calls]:
assert len(calls) == 1

# create for customer with m2 price
assert create_calls[0]["args"] == ("cus-1",)
assert create_calls[0]["kwargs"]["price_id"] == m2.stripe_price_id
assert create_calls[0]["kwargs"]["default_payment_method"] is not None
assert create_calls[0]["kwargs"]["backdate"] is False

# cancel sub1
assert cancel_calls[0]["args"] == ("sub1",)
assert cancel_calls[0]["kwargs"]["cancel_immediately"] is False


@pytest.mark.freeze_time("2024-02-12")
@patch("booking.models.membership_models.StripeConnector", MockConnector)
@patch("booking.views.membership_views.StripeConnector")
def test_membership_change_post_future_subscription(mock_conn, client, seller, configured_stripe_user):
mock_connector = MockConnector()
mock_conn.return_value = mock_connector

client.force_login(configured_stripe_user)
m1 = baker.make(Membership, name="m1", active=True)
m2 = baker.make(Membership, name="m2", active=True)
m2.stripe_price_id = "price_2345"
m2.save()
baker.make(
UserMembership,
user=configured_stripe_user,
membership=m1,
subscription_id="sub1",
subscription_status="active",
subscription_start_date=datetime(2024, 2, 10),
subscription_billing_cycle_anchor=datetime(2024, 2, 25),
start_date=datetime(2024, 3, 1)
)

resp = client.post(reverse("membership_change", args=("sub1",)), {"membership": m2.id})
assert resp.status_code == 302

create_calls = mock_connector.method_calls["create_subscription"]
cancel_calls = mock_connector.method_calls["cancel_subscription"]
for calls in [create_calls, cancel_calls]:
assert len(calls) == 1

# create for customer with m2 price
assert create_calls[0]["args"] == ("cus-1",)
assert create_calls[0]["kwargs"]["price_id"] == m2.stripe_price_id
assert create_calls[0]["kwargs"]["default_payment_method"] is not None
assert create_calls[0]["kwargs"]["backdate"] is False

# cancel sub1
assert cancel_calls[0]["args"] == ("sub1",)
assert cancel_calls[0]["kwargs"]["cancel_immediately"] is True


@patch("booking.models.membership_models.StripeConnector", MockConnector)
def test_membership_change_post_invalid_form(client, seller, configured_stripe_user):
client.force_login(configured_stripe_user)
m1 = baker.make(Membership, name="m1", active=True)
baker.make(
UserMembership,
user=configured_stripe_user,
membership=m1,
subscription_id="sub1",
)
resp = client.post(reverse("membership_change", args=("sub1",)), {"membership": "foo"})
assert resp.status_code == 200
24 changes: 13 additions & 11 deletions booking/views/membership_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,22 @@ def membership_change(request, subscription_id):
old_membership = user_membership.membership
can_change = not bool(user_membership.end_date)

# can only change IF the membership is active and not cancelling, i.e. it has no end date
# changes will all start from the beginning of the next month
# cancellations will start from the beginning of the next month
# if it's the user's current (uncancelled) membership, this one will be cancelled from the end of the month and
# a new one created from start of next month
# if it's a future membership (starting at beginning of next month):
# - if it's not billed yet, it'll be cancelled from the end of the month (25th) and a new one created that starts
# from the start of next month - so this one should never be invoiced.

if request.method == "POST":
form = ChangeMembershipForm(request.POST, current_membership_id=user_membership.membership.id)
if form.is_valid():
membership = form.cleaned_data["membership"]
client = StripeConnector(request)
current_subscription = client.get_subscription(subscription_id)
new_subscription = client.create_subscription(
client.create_subscription(
request.user.userprofile.stripe_customer_id, price_id=membership.stripe_price_id, backdate=False,
default_payment_method=current_subscription.default_payment_method,
)
Expand All @@ -98,17 +107,10 @@ def membership_change(request, subscription_id):

return HttpResponseRedirect(reverse("membership_list"))

else:
# can only change IF the membership is active and not cancelling, i.e. it has no end date
# changes will all start from the beginning of the next month
# cancellations will start from the beginning of the next month
# if it's the user's current (uncancelled) membership, this one will be cancelled from the end of the month and
# a new one created from start of next month
# if it's a future membership (starting at beginning of next month):
# - if it's not billed yet, it'll be cancelled from the end of the month (25th) and a new one created that starts
# from the start of next month - so this one should never be invoiced.
# - if it's been billed (i.e. between 25th and 1st), can't change? or changes are one month later?
elif can_change:
form = ChangeMembershipForm(current_membership_id=user_membership.membership.id)
else:
form = None
return TemplateResponse(
request,
"booking/membership_change.html",
Expand Down
43 changes: 31 additions & 12 deletions stripe_payments/tests/mock_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,69 @@ class MockConnector:
def __init__(self, *args):
self.connected_account = None
self.connected_account_id = "id123"
self.method_calls = {}

def create_stripe_product(self, product_id, name, description, price):
def _record(self, fn, *args, **kwargs):
self.method_calls.setdefault(fn.__name__, []).append({"args": args, "kwargs": kwargs})

def create_stripe_product(self, *args, **kwargs):
self._record(self.create_stripe_product, *args, **kwargs)
return MagicMock(spec=stripe.Product, default_price="price_1234")

def update_stripe_product(self, product_id, name, description, active, price_id):
def update_stripe_product(self, *args, **kwargs):
self._record(self.update_stripe_product, *args, **kwargs)
return MagicMock(spec=stripe.Product, default_price="price_2345")

def get_or_create_stripe_price(self, product_id, price):
def get_or_create_stripe_price(self, *args, **kwargs):
self._record(self.get_or_create_stripe_price, *args, **kwargs)
return "price_234"

def get_or_create_stripe_customer(self, user, **kwargs):
def get_or_create_stripe_customer(self, *args, **kwargs):
self._record(self.get_or_create_stripe_customer, *args, **kwargs)
user = args[0]
if user.userprofile.stripe_customer_id:
return user.userprofile.stripe_customer_id

user.userprofile.stripe_customer_id = "cus_234"
user.userprofile.save()
return "cus_234"

def update_stripe_customer(self, customer_id, **kwargs):
raise NotImplementedError
def update_stripe_customer(self, *args, **kwargs):
self._record(self.update_stripe_customer, *args, **kwargs)

def get_subscription(self, subscription_id):
self._record(self.get_subscription, subscription_id)
return MagicMock(id=subscription_id)

def get_setup_intent(self, setup_intent_id):
raise NotImplementedError
self._record(self.get_setup_intent, setup_intent_id)

def create_subscription(self, customer_id, price_id, backdate=True):
raise NotImplementedError
def create_subscription(self, *args, **kwargs):
self._record(self.create_subscription, *args, **kwargs)

return MagicMock(spec=stripe.Subscription, default_payment_method=kwargs.get("default_payment_method"))

def get_or_create_subscription_schedule(self, subscription_id):
raise NotImplementedError
self._record(self.get_or_create_subscription_schedule, subscription_id)

def update_subscription_price(self, subscription_id, new_price_id):
self._record(self.get_or_create_subscription_schedule, subscription_id, new_price_id)
raise NotImplementedError

def cancel_subscription(self, subscription_id):
raise NotImplementedError
def cancel_subscription(self, subscription_id, cancel_immediately=False):
self._record(self.cancel_subscription, subscription_id, cancel_immediately=cancel_immediately)
return MagicMock(
id=subscription_id, status="canceled" if cancel_immediately else "active",
)

def add_discount_to_subscription(self, subscription_id):
self._record(self.add_discount_to_subscription, [subscription_id])
raise NotImplementedError

def customer_portal_configuration(self):
self._record(self.customer_portal_configuration)
raise NotImplementedError

def customer_portal_url(self, customer_id):
self._record(self.customer_portal_url, [customer_id])
raise NotImplementedError

0 comments on commit 7e69105

Please sign in to comment.