Skip to content

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

Open
srijantrpth wants to merge 8 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 8 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

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 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


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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 == "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(
    ...
):
     ...
     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"]
-
-    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

@Zaimwa9

Zaimwa9 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

@themis-blindfold review

Comment thread api/tests/unit/organisations/test_unit_organisations_tasks.py Outdated
@themis-blindfold

Copy link
Copy Markdown

⚖️ Themis judgement: 🟠 Fix before merge

The production code change is well-motivated: switching the notification threshold evaluation from the potentially stale subscription_information_cache.allowed_30d_api_calls to the authoritative subscription.max_api_calls fixes the premature-alert bug described in #7976. The filter in handle_api_usage_notifications and the denominator in handle_api_usage_notification_for_organisation are updated consistently, and the regression test covers the exact stale-cache scenario. However, one existing test was not updated to match the new filter field and will fail.

🎯 Correctness 3/5
🧪 Test coverage 3/5
📐 Code quality 4/5
🚀 Product impact 4/5

🟠 Majors

  • api/tests/unit/organisations/test_unit_organisations_tasks.pytest_handle_api_usage_notifications__processing_error__logs_error_message will fail because max_api_calls is not set on the subscription. See inline comment.

🧹 Nits

  • api/tests/unit/organisations/test_unit_organisations_tasks.py:434 — comment # Updated to fix broken test references a transient event and will confuse future readers. Either remove it or describe the constraint (e.g. "must match cache value for the pre-filter").

⚖️ Acknowledged

  • Tests and fixtures need updating to set subscription.max_api_calls — thread resolved by @srijantrpth.
  • Add regression test where cache and subscription disagree — thread resolved by @srijantrpth.
📝 Walkthrough
  • Notification pre-filter (tasks.py) — changed the queryset filter from subscription_information_cache__allowed_30d_api_calls to subscription__max_api_calls so the pre-filter uses the authoritative plan limit, not a potentially stale cache.
  • Notification denominator (task_helpers.py) — changed the else branch (paid plans) to use organisation.subscription.max_api_calls for the usage-percentage calculation, matching the filter change.
  • Test updates — set max_api_calls on subscriptions in affected tests and added a stale-cache regression test that verifies usage evaluated against the true plan limit (50,000) does not trigger a premature alert.
🧪 How to verify
  1. Run pytest api/tests/unit/organisations/test_unit_organisations_tasks.py -k "handle_api_usage_notifications" -x and confirm all tests pass (the processing-error test will currently fail).
  2. After applying the fix, re-run the same command and confirm all tests pass.
  3. Verify no other tests in the file reference allowed_30d_api_calls without a corresponding max_api_calls setup: grep -n "allowed_30d_api_calls" api/tests/unit/organisations/test_unit_organisations_tasks.py.

Automate: the existing test suite covers this once the missed fixture is fixed.

Product take: This is a solid improvement for paid-plan users who were receiving premature API usage warning emails due to stale cache data. Prevents unnecessary alarm and support burden from false-positive notifications.

Big picture: The notification path now uses subscription.max_api_calls while the sibling overage-billing (charge_for_api_call_count_overages) and access-restriction (restrict_use_due_to_api_limit_grace_period_over) paths still use subscription_cache.allowed_30d_api_calls. If cache staleness caused incorrect notifications, the same staleness could affect billing and access decisions. Worth evaluating whether those paths should also move to the authoritative subscription value.

🧭 Assumptions & unverified claims
  • The review assumes subscription.max_api_calls is reliably updated at plan-change time via update_plan() and does not itself go stale. The codebase shows update_plan() sets it, but whether all plan-change flows route through that method was not exhaustively verified.
  • CI test results were not available in the check snapshot (only label, linting, and Vercel deployment checks completed). The Vercel deployment failures appear to be authorization-related, unrelated to this PR.

The cache giveth and the cache taketh away — but at least now it's only the subscription's word that matters. · reviewed at b475015

1 finding(s) anchored outside the diff (not posted inline)
  • api/tests/unit/organisations/test_unit_organisations_tasks.py:740 🟠 Major · ⚡ Quick win

Test will fail: max_api_calls not set, organisation filtered out by pre-filter

Observed: this test sets allowed_30d_api_calls=100 and api_calls_30d=100 on the cache, but does not set subscription.max_api_calls. The field defaults to MAX_API_CALLS_IN_FREE_PLAN (50,000). Predicted: the new filter api_calls_30d >= max_api_calls * 0.75 evaluates as 100 >= 37,500 which is false, so the organisation is excluded from the queryset and handle_api_usage_notification_for_organisation is never called. The assert_called_once_with on line 764 would fail.

Fix: add organisation.subscription.max_api_calls = 100 before the save, matching the pattern used in the other updated tests.

    organisation.subscription.subscription_id = "fancy_id"
    organisation.subscription.max_api_calls = 100
    organisation.subscription.save()

@srijantrpth

Copy link
Copy Markdown
Author

Hi @Zaimwa9 , I've pushed the updates to fix the failing test and removed the transient comment flagged by the bot. Let me know if anything else is needed!.

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.49%. Comparing base (26a1e29) to head (5838b03).
⚠️ Report is 31 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8003      +/-   ##
==========================================
- Coverage   98.63%   98.49%   -0.14%     
==========================================
  Files        1496     1506      +10     
  Lines       59131    59592     +461     
==========================================
+ Hits        58325    58697     +372     
- Misses        806      895      +89     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

3 participants