From bef0fac32e3a7549d3b3b25c0c1f5d22ce8109a4 Mon Sep 17 00:00:00 2001 From: srijantrpth Date: Tue, 14 Jul 2026 15:33:25 +0530 Subject: [PATCH 1/5] Fix API usage notification threshold calculation (#7976) --- api/organisations/task_helpers.py | 2 +- api/organisations/tasks.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/organisations/task_helpers.py b/api/organisations/task_helpers.py index 0062633322dc..80bac5d0f9e1 100644 --- a/api/organisations/task_helpers.py +++ b/api/organisations/task_helpers.py @@ -125,7 +125,7 @@ def handle_api_usage_notification_for_organisation(organisation: Organisation) - month_delta = _get_total_months(relativedelta(now, billing_starts_at)) period_starts_at = relativedelta(months=month_delta) + billing_starts_at - allowed_api_calls = subscription_cache.allowed_30d_api_calls + allowed_api_calls = organisation.subscription.max_api_calls openfeature_client = get_openfeature_client() # TODO: Default to get_total_events_count — https://github.com/Flagsmith/flagsmith/issues/6985 diff --git a/api/organisations/tasks.py b/api/organisations/tasks.py index f7a00ef62926..5951dc0c7b66 100644 --- a/api/organisations/tasks.py +++ b/api/organisations/tasks.py @@ -131,7 +131,7 @@ def handle_api_usage_notifications() -> None: # threshold in the last 30d, it must be below it for the current billing period # (which by definition is <30d). subscription_information_cache__api_calls_30d__gte=F( - "subscription_information_cache__allowed_30d_api_calls" + "subscription__max_api_calls" ) * threshold_percentage, ).select_related("subscription", "subscription_information_cache"): From ca8c7f4f3208da61c0b9558c447b23f41a6d39e0 Mon Sep 17 00:00:00 2001 From: srijantrpth Date: Tue, 14 Jul 2026 15:47:22 +0530 Subject: [PATCH 2/5] Update tests to align with actual plan limit evaluations --- .../test_unit_organisations_tasks.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/api/tests/unit/organisations/test_unit_organisations_tasks.py b/api/tests/unit/organisations/test_unit_organisations_tasks.py index d953ed2845e3..36c941a22616 100644 --- a/api/tests/unit/organisations/test_unit_organisations_tasks.py +++ b/api/tests/unit/organisations/test_unit_organisations_tasks.py @@ -431,6 +431,7 @@ def test_handle_api_usage_notifications__usage_below_100_percent__sends_90_perce now = timezone.now() organisation.subscription.plan = SCALE_UP organisation.subscription.subscription_id = "fancy_id" + organisation.subscription.max_api_calls = 100 organisation.subscription.save() OrganisationSubscriptionInformationCache.objects.create( organisation=organisation, @@ -496,7 +497,58 @@ def test_handle_api_usage_notifications__usage_below_100_percent__sends_90_perce "usage_url": f"{get_current_site_url()}/organisation/{organisation.id}/usage", }, ) +@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") +def test_handle_api_usage_notifications__stale_cache_lower_than_subscription_limit__sends_no_email( + mocker: MockerFixture, + organisation: Organisation, + mailoutbox: list[EmailMultiAlternatives], + enable_features: EnableFeaturesFixture, +) -> None: + # Given + now = timezone.now() + usage = 95 + stale_cache_allowance = 100 + true_subscription_allowance = 50_000 + + # 1. Set the TRUE subscription limit to 50,000 + organisation.subscription.plan = SCALE_UP + organisation.subscription.subscription_id = "fancy_id" + organisation.subscription.max_api_calls = true_subscription_allowance + organisation.subscription.save() + + # 2. Set the STALE cache limit to 100 + OrganisationSubscriptionInformationCache.objects.create( + organisation=organisation, + allowed_30d_api_calls=stale_cache_allowance, + current_billing_term_starts_at=now - timedelta(days=45), + current_billing_term_ends_at=now + timedelta(days=320), + api_calls_30d=usage, + ) + + mock_api_usage = mocker.patch( + "organisations.task_helpers.get_current_api_usage", + ) + mock_api_usage.return_value = usage + enable_features("api_usage_alerting") + + assert not OrganisationAPIUsageNotification.objects.filter( + organisation=organisation, + ).exists() + + # When + handle_api_usage_notifications() + # Then + # Because usage (95) is evaluated against the true allowance (50,000), + # it is < 1%, so no notification should be sent. + assert len(mailoutbox) == 0 + + assert ( + OrganisationAPIUsageNotification.objects.filter( + organisation=organisation, + ).count() + == 0 + ) assert email.from_email == "noreply@flagsmith.com" # Only admin because threshold is under 100. assert email.to == ["admin@example.com"] From 73fb7ad25ef95fab9e1ba22dc52ab622dba22872 Mon Sep 17 00:00:00 2001 From: srijantrpth Date: Tue, 14 Jul 2026 15:59:53 +0530 Subject: [PATCH 3/5] Fix test boundaries caught by CodeRabbit review --- .../test_unit_organisations_tasks.py | 39 +++---------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/api/tests/unit/organisations/test_unit_organisations_tasks.py b/api/tests/unit/organisations/test_unit_organisations_tasks.py index 36c941a22616..4fa7d5f348c9 100644 --- a/api/tests/unit/organisations/test_unit_organisations_tasks.py +++ b/api/tests/unit/organisations/test_unit_organisations_tasks.py @@ -431,7 +431,7 @@ def test_handle_api_usage_notifications__usage_below_100_percent__sends_90_perce now = timezone.now() organisation.subscription.plan = SCALE_UP organisation.subscription.subscription_id = "fancy_id" - organisation.subscription.max_api_calls = 100 + organisation.subscription.max_api_calls = 100 # Updated to fix broken test organisation.subscription.save() OrganisationSubscriptionInformationCache.objects.create( organisation=organisation, @@ -497,6 +497,8 @@ def test_handle_api_usage_notifications__usage_below_100_percent__sends_90_perce "usage_url": f"{get_current_site_url()}/organisation/{organisation.id}/usage", }, ) + + @pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_handle_api_usage_notifications__stale_cache_lower_than_subscription_limit__sends_no_email( mocker: MockerFixture, @@ -524,7 +526,7 @@ def test_handle_api_usage_notifications__stale_cache_lower_than_subscription_lim current_billing_term_ends_at=now + timedelta(days=320), api_calls_30d=usage, ) - + mock_api_usage = mocker.patch( "organisations.task_helpers.get_current_api_usage", ) @@ -539,7 +541,7 @@ def test_handle_api_usage_notifications__stale_cache_lower_than_subscription_lim handle_api_usage_notifications() # Then - # Because usage (95) is evaluated against the true allowance (50,000), + # Because usage (95) is evaluated against the true allowance (50,000), # it is < 1%, so no notification should be sent. assert len(mailoutbox) == 0 @@ -549,37 +551,6 @@ def test_handle_api_usage_notifications__stale_cache_lower_than_subscription_lim ).count() == 0 ) - assert email.from_email == "noreply@flagsmith.com" - # Only admin because threshold is under 100. - assert email.to == ["admin@example.com"] - - assert ( - OrganisationAPIUsageNotification.objects.filter( - organisation=organisation, - ).count() - == 1 - ) - api_usage_notification = OrganisationAPIUsageNotification.objects.filter( - organisation=organisation, - ).first() - - assert api_usage_notification.percent_usage == 90 # type: ignore[union-attr] - - # Now re-run the usage to make sure the notification isn't resent. - handle_api_usage_notifications() - - assert ( - OrganisationAPIUsageNotification.objects.filter( - organisation=organisation, - ).count() - == 1 - ) - assert ( - OrganisationAPIUsageNotification.objects.filter( - organisation=organisation - ).first() - == api_usage_notification - ) @pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") From 99fc1aa7b18adbe4c31580b259887e702e3d2eeb Mon Sep 17 00:00:00 2001 From: srijantrpth Date: Tue, 14 Jul 2026 16:04:49 +0530 Subject: [PATCH 4/5] Update tests to align with actual plan limit evaluations --- .../test_unit_organisations_tasks.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/api/tests/unit/organisations/test_unit_organisations_tasks.py b/api/tests/unit/organisations/test_unit_organisations_tasks.py index 4fa7d5f348c9..95e22d94fb07 100644 --- a/api/tests/unit/organisations/test_unit_organisations_tasks.py +++ b/api/tests/unit/organisations/test_unit_organisations_tasks.py @@ -498,6 +498,38 @@ def test_handle_api_usage_notifications__usage_below_100_percent__sends_90_perce }, ) + assert email.from_email == "noreply@flagsmith.com" + # Only admin because threshold is under 100. + assert email.to == ["admin@example.com"] + + assert ( + OrganisationAPIUsageNotification.objects.filter( + organisation=organisation, + ).count() + == 1 + ) + api_usage_notification = OrganisationAPIUsageNotification.objects.filter( + organisation=organisation, + ).first() + + assert api_usage_notification.percent_usage == 90 # type: ignore[union-attr] + + # Now re-run the usage to make sure the notification isn't resent. + handle_api_usage_notifications() + + assert ( + OrganisationAPIUsageNotification.objects.filter( + organisation=organisation, + ).count() + == 1 + ) + assert ( + OrganisationAPIUsageNotification.objects.filter( + organisation=organisation + ).first() + == api_usage_notification + ) + @pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00") def test_handle_api_usage_notifications__stale_cache_lower_than_subscription_limit__sends_no_email( @@ -615,6 +647,7 @@ def test_handle_api_usage_notifications__usage_above_100_percent__sends_limit_no now = timezone.now() organisation.subscription.plan = SCALE_UP organisation.subscription.subscription_id = "fancy_id" + organisation.subscription.max_api_calls = allowance organisation.subscription.save() OrganisationSubscriptionInformationCache.objects.create( organisation=organisation, @@ -758,6 +791,8 @@ def test_handle_api_usage_notifications__free_account_over_limit__sends_limit_no assert organisation.is_paid is False assert organisation.subscription.is_free_plan is True assert organisation.subscription.max_api_calls == MAX_API_CALLS_IN_FREE_PLAN + organisation.subscription.max_api_calls = MAX_API_CALLS_IN_FREE_PLAN + organisation.subscription.save() OrganisationSubscriptionInformationCache.objects.create( organisation=organisation, From b47501555f1ff30e96dca374f7d1b0a880b82a2d Mon Sep 17 00:00:00 2001 From: srijantrpth Date: Tue, 14 Jul 2026 16:11:29 +0530 Subject: [PATCH 5/5] Strengthen regression test with mock assertion --- api/tests/unit/organisations/test_unit_organisations_tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/tests/unit/organisations/test_unit_organisations_tasks.py b/api/tests/unit/organisations/test_unit_organisations_tasks.py index 95e22d94fb07..fc9ab8ff75b6 100644 --- a/api/tests/unit/organisations/test_unit_organisations_tasks.py +++ b/api/tests/unit/organisations/test_unit_organisations_tasks.py @@ -575,6 +575,7 @@ def test_handle_api_usage_notifications__stale_cache_lower_than_subscription_lim # Then # Because usage (95) is evaluated against the true allowance (50,000), # it is < 1%, so no notification should be sent. + mock_api_usage.assert_not_called() assert len(mailoutbox) == 0 assert (