From 603889c88f5ffed12c4d5be269ea1ed6d5966042 Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Mon, 9 Sep 2024 11:53:00 -0400 Subject: [PATCH] fix: Handle null cancellation dates (#4589) --- .../chargebee/webhook_handlers.py | 11 +++-- .../test_unit_organisations_views.py | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/api/organisations/chargebee/webhook_handlers.py b/api/organisations/chargebee/webhook_handlers.py index 52bc110e293e..1d8d6dd40a30 100644 --- a/api/organisations/chargebee/webhook_handlers.py +++ b/api/organisations/chargebee/webhook_handlers.py @@ -126,10 +126,15 @@ def process_subscription(request: Request) -> Response: # noqa: C901 return Response(status=status.HTTP_200_OK) if subscription["status"] in ("non_renewing", "cancelled"): - existing_subscription.prepare_for_cancel( - datetime.fromtimestamp(subscription.get("current_term_end")).replace( + cancellation_date = subscription.get("current_term_end") + if cancellation_date is not None: + cancellation_date = datetime.fromtimestamp(cancellation_date).replace( tzinfo=timezone.utc - ), + ) + else: + cancellation_date = timezone.now() + existing_subscription.prepare_for_cancel( + cancellation_date, update_chargebee=False, ) return Response(status=status.HTTP_200_OK) diff --git a/api/tests/unit/organisations/test_unit_organisations_views.py b/api/tests/unit/organisations/test_unit_organisations_views.py index ceae4eb079cb..ddade0bd039a 100644 --- a/api/tests/unit/organisations/test_unit_organisations_views.py +++ b/api/tests/unit/organisations/test_unit_organisations_views.py @@ -38,6 +38,7 @@ from organisations.permissions.permissions import CREATE_PROJECT from organisations.subscriptions.constants import ( CHARGEBEE, + FREE_PLAN_ID, MAX_API_CALLS_IN_FREE_PLAN, MAX_SEATS_IN_FREE_PLAN, SUBSCRIPTION_BILLING_STATUS_ACTIVE, @@ -635,6 +636,7 @@ def test_when_subscription_is_set_to_non_renewing_then_cancellation_date_set_and current_term_end = int(datetime.timestamp(cancellation_date)) subscription.subscription_id = "subscription-id" subscription.save() + data = { "content": { "subscription": { @@ -663,6 +665,44 @@ def test_when_subscription_is_set_to_non_renewing_then_cancellation_date_set_and mocked_cancel_chargebee_subscription.assert_not_called() +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") +@mock.patch("organisations.models.cancel_chargebee_subscription") +def test_when_subscription_is_set_to_non_renewing_then_cancellation_date_set_and_current_term_end_is_missing( + mocked_cancel_chargebee_subscription: MagicMock, + chargebee_subscription: Subscription, + staff_user: FFAdminUser, + staff_client: APIClient, + settings: SettingsWrapper, +) -> None: + # Given + data = { + "content": { + "subscription": { + "status": "non_renewing", + "id": chargebee_subscription.subscription_id, + # Note the missing current_term_end field. + }, + "customer": {"email": staff_user.email}, + } + } + url = reverse("api-v1:chargebee-webhook") + + settings.ORG_SUBSCRIPTION_CANCELLED_ALERT_RECIPIENT_LIST = ["foo@bar.com"] + + # When + staff_client.post(url, data=json.dumps(data), content_type="application/json") + + # Then + chargebee_subscription.refresh_from_db() + # Cancellation date is set to None because the missing current_term_end field + # means that the cancellation is processed immediately and the subscription + # reverts to being a free plan, so there's no more cancelation date set. + assert chargebee_subscription.cancellation_date is None + assert chargebee_subscription.plan == FREE_PLAN_ID + assert len(mail.outbox) == 1 + mocked_cancel_chargebee_subscription.assert_not_called() + + def test_when_subscription_is_cancelled_then_cancellation_date_set_and_alert_sent( subscription: Subscription, staff_user: FFAdminUser,