Skip to content

Fix (Billing): API usage notification evaluates against actual plan limit instead of cache#8003

Open
srijantrpth wants to merge 5 commits into
Flagsmith:mainfrom
srijantrpth:fix/issue-7976-api-notification-math
Open

Fix (Billing): API usage notification evaluates against actual plan limit instead of cache#8003
srijantrpth wants to merge 5 commits into
Flagsmith:mainfrom
srijantrpth:fix/issue-7976-api-notification-math

Conversation

@srijantrpth

Copy link
Copy Markdown

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Closes #7976

Organisations on paid plans were receiving API usage warning emails prematurely. The handle_api_usage_notifications task was evaluating usage against the subscription_information_cache rather than the true plan limit. Because the cache can become stale, the percentage evaluated artificially high.

  • Updated the task filter in handle_api_usage_notifications to use subscription__max_api_calls.
  • Updated the denominator in the else block of handle_api_usage_notification_for_organisation to bypass the cache and use organisation.subscription.max_api_calls directly.

Review effort: 1/5

How did you test this code?

  • Ran make format to ensure code style compliance.
  • Verified the logic path tracing back to the core database models.
  • Relying on the automated GitHub Actions CI/CD pipeline to execute the tests/unit/organisations/ test suite, as my local Docker test-database permissions are currently restricted.

Copilot AI review requested due to automatic review settings July 14, 2026 10:08
@srijantrpth srijantrpth requested a review from a team as a code owner July 14, 2026 10:08
@srijantrpth srijantrpth requested review from khvn26 and removed request for a team July 14, 2026 10:08
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

@srijantrpth is attempting to deploy a commit to the Flagsmith Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the api Issue related to the REST API label Jul 14, 2026
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ad002a90-395c-47ee-b4e7-e0388a2917a9

📥 Commits

Reviewing files that changed from the base of the PR and between 99fc1aa and b475015.

📒 Files selected for processing (1)
  • api/tests/unit/organisations/test_unit_organisations_tasks.py

📝 Walkthrough

Walkthrough

API usage notification eligibility now compares usage against subscription.max_api_calls instead of the cached 30-day allowance. Notification percentage calculations also use organisation.subscription.max_api_calls, with tests covering explicit subscription limits and stale cache behaviour.

Estimated code review effort: 2 (Simple) | ~10 minutes


Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes premature API usage warning emails for paid organisations by ensuring the usage-threshold evaluation uses the authoritative subscription plan limit rather than potentially stale cached subscription information.

Changes:

  • Updates handle_api_usage_notifications to filter organisations using subscription.max_api_calls (plan limit) instead of the subscription information cache.
  • Updates handle_api_usage_notification_for_organisation to compute the allowance using organisation.subscription.max_api_calls rather than the cached allowed_30d_api_calls.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
api/organisations/tasks.py Updates the notification task’s pre-filter to compare 30-day usage against the actual subscription plan API call limit.
api/organisations/task_helpers.py Ensures the percentage calculation uses the subscription’s max_api_calls directly instead of the cached allowance value.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/organisations/tasks.py
Comment thread api/organisations/task_helpers.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/tests/unit/organisations/test_unit_organisations_tasks.py (1)

501-584: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Critical: new test absorbs the previous test's tail, breaking both tests.

The new test's assertion block ends at Line 553, but Lines 554-584 (which reference email, api_usage_notification, and re-run the notification check) are unchanged code left over from test_handle_api_usage_notifications__usage_below_100_percent__sends_90_percent_notification (Lines 424-499). That test's actual closing assertions were never reattached after the earlier lines were split off, so:

  • test_handle_api_usage_notifications__stale_cache_lower_than_subscription_limit__sends_no_email now inherits Lines 554-584 into its own body and will raise NameError: name 'email' is not defined at runtime, since this test never assigns email (only checks len(mailoutbox) == 0).
  • The 90%-notification test lost its email/idempotency assertions entirely.

This will break collection/execution of this test module. Reattach the dangling tail to the first test and cleanly close the new test at Line 553.

🐛 Proposed fix to restore correct test boundaries
     assert email.alternatives[0][0] == render_to_string(
         "organisations/api_usage_notification.html",
         context={
             "organisation": organisation,
             "matched_threshold": 90,
             "usage_url": f"{get_current_site_url()}/organisation/{organisation.id}/usage",
         },
     )
+    assert email.from_email == "[email protected]"
+    # Only admin because threshold is under 100.
+    assert email.to == ["[email protected]"]
+
+    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(
    ...
):
     ...
     assert (
         OrganisationAPIUsageNotification.objects.filter(
             organisation=organisation,
         ).count()
         == 0
     )
-    assert email.from_email == "[email protected]"
-    # Only admin because threshold is under 100.
-    assert email.to == ["[email protected]"]
-
-    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
-    )

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1e9dd86e-2a7c-4e5e-874f-545d5e2d6d98

📥 Commits

Reviewing files that changed from the base of the PR and between bef0fac and 3a5e584.

📒 Files selected for processing (1)
  • api/tests/unit/organisations/test_unit_organisations_tasks.py

@srijantrpth srijantrpth force-pushed the fix/issue-7976-api-notification-math branch from 3a5e584 to 99fc1aa Compare July 14, 2026 10:35

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/tests/unit/organisations/test_unit_organisations_tasks.py (1)

535-585: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the org is excluded at the queryset stage, not just that no email was sent.

This test correctly reproduces the #7976 bug pattern (stale cache close to threshold vs. a much higher true max_api_calls), but it only checks the downstream symptom (no email/notification). Since the fix operates by excluding the organisation via the subscription__max_api_calls-based queryset filter in handle_api_usage_notifications, the org should never reach get_current_api_usage at all. Add mock_api_usage.assert_not_called() to pin down that the mechanism (queryset exclusion), not just its effect, works as intended — this also clarifies that mock_api_usage.return_value = usage is otherwise unused here.

✅ Suggested strengthening
     # Then
+    mock_api_usage.assert_not_called()
+
     # Because usage (95) is evaluated against the true allowance (50,000),
     # it is < 1%, so no notification should be sent.
     assert len(mailoutbox) == 0

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4812c45b-9bcc-4ec5-b8dc-b01df14161f4

📥 Commits

Reviewing files that changed from the base of the PR and between 3a5e584 and 99fc1aa.

📒 Files selected for processing (1)
  • api/tests/unit/organisations/test_unit_organisations_tasks.py

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Issue related to the REST API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

API usage notification emails fire at 90%/100% while org is far below plan limit

2 participants