Skip to content

feat: get plan bulk with cache#161

Open
tomerqodo wants to merge 4 commits intoqodo_claude_vs_qodo_base_feat_get_plan_bulk_with_cache_pr10from
qodo_claude_vs_qodo_head_feat_get_plan_bulk_with_cache_pr10
Open

feat: get plan bulk with cache#161
tomerqodo wants to merge 4 commits intoqodo_claude_vs_qodo_base_feat_get_plan_bulk_with_cache_pr10from
qodo_claude_vs_qodo_head_feat_get_plan_bulk_with_cache_pr10

Conversation

@tomerqodo
Copy link

Benchmark PR from agentic-review-benchmarks#10

@qodo-code-review
Copy link

Review Summary by Qodo

Add Redis caching for bulk plan retrieval with fallback

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Add caching layer to get_plan_bulk with Redis to reduce billing API load
• Implement get_plan_bulk_with_cache method with fallback to API on cache miss
• Add comprehensive integration and unit tests for cache scenarios
• Improve error handling in get_plan_bulk with per-tenant validation
Diagram
flowchart LR
  A["Bulk Plan Request"] --> B["Check Redis Cache"]
  B --> C{Cache Hit?}
  C -->|Yes| D["Return Cached Plans"]
  C -->|No| E["Fetch from Billing API"]
  E --> F["Validate Plans"]
  F --> G["Store in Redis"]
  G --> H["Return Plans"]
  D --> H
Loading

Grey Divider

File Changes

1. api/services/billing_service.py ✨ Enhancement +109/-3

Add Redis caching layer for bulk plan retrieval

• Add json import for JSON serialization
• Add __str__ method to SubscriptionPlan TypedDict for debugging
• Add Redis cache configuration constants (_PLAN_CACHE_KEY_PREFIX, _PLAN_CACHE_TTL)
• Enhance get_plan_bulk with per-tenant error handling and validation
• Implement new get_plan_bulk_with_cache method with Redis caching and fallback logic
• Add _make_plan_cache_key helper method for cache key generation

api/services/billing_service.py


2. api/tests/test_containers_integration_tests/services/test_billing_service.py 🧪 Tests +365/-0

Add integration tests for cached plan retrieval

• Create comprehensive integration test suite for get_plan_bulk_with_cache
• Test all cache scenarios: full hit, full miss, partial hit
• Test Redis failure modes and fallback behavior
• Test invalid cache data handling and TTL expiration
• Test pipeline failures and empty input handling

api/tests/test_containers_integration_tests/services/test_billing_service.py


3. api/tests/unit_tests/services/test_billing_service.py 🧪 Tests +36/-0

Add unit test for invalid plan validation

• Add unit test for get_plan_bulk with invalid tenant plan data
• Test that invalid plans are skipped and logged appropriately
• Verify valid tenants are still returned correctly

api/tests/unit_tests/services/test_billing_service.py


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 10, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (3) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Integration tests missing annotations 📘 Rule violation ✓ Correctness
Description
New test helper and test functions are missing required type annotations (parameters and/or return
types). This violates the rule requiring explicit type hints for all Python function definitions.
Code

api/tests/test_containers_integration_tests/services/test_billing_service.py[R23-40]

+    def setup_redis_cleanup(self, flask_app_with_containers):
+        """Clean up Redis cache before and after each test."""
+        with flask_app_with_containers.app_context():
+            # Clean up before test
+            yield
+            # Clean up after test
+            # Delete all test cache keys
+            pattern = f"{BillingService._PLAN_CACHE_KEY_PREFIX}*"
+            keys = redis_client.keys(pattern)
+            if keys:
+                redis_client.delete(*keys)
+
+    def _create_test_plan_data(self, plan: str = "sandbox", expiration_date: int = 1735689600):
+        """Helper to create test SubscriptionPlan data."""
+        return {"plan": plan, "expiration_date": expiration_date}
+
+    def _set_cache(self, tenant_id: str, plan_data: dict, ttl: int = 600):
+        """Helper to set cache data in Redis."""
Evidence
The checklist requires type hints for every function parameter and return type, but multiple
newly-added functions lack return annotations and/or typed parameters (e.g., fixture, helpers).

AGENTS.md
api/tests/test_containers_integration_tests/services/test_billing_service.py[23-24]
api/tests/test_containers_integration_tests/services/test_billing_service.py[35-37]
api/tests/test_containers_integration_tests/services/test_billing_service.py[39-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several newly-added test functions/helpers lack required type annotations (parameters and return types).

## Issue Context
The repository requires explicit type annotations for all Python function definitions.

## Fix Focus Areas
- api/tests/test_containers_integration_tests/services/test_billing_service.py[23-24]
- api/tests/test_containers_integration_tests/services/test_billing_service.py[35-37]
- api/tests/test_containers_integration_tests/services/test_billing_service.py[39-43]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. None plan returned 🐞 Bug ✓ Correctness
Description
BillingService.get_plan_bulk stores None for tenants whose plan payload fails validation even though
it declares returning dict[str, SubscriptionPlan]. This will either break callers that index into
the plan (e.g., plan['plan']) or fail the new unit test that expects invalid tenants to be omitted.
Code

api/services/billing_service.py[R285-292]

+                    try:
+                        subscription_plan = subscription_adapter.validate_python(plan)
+                        results[tenant_id] = subscription_plan
+                    except Exception:
+                        logger.exception(
+                            "get_plan_bulk: failed to validate subscription plan for tenant(%s)", tenant_id
+                        )
+                        results[tenant_id] = None
Evidence
The implementation explicitly assigns None into a dict annotated and documented as mapping to
SubscriptionPlan, and the newly added unit test asserts the invalid tenant key should not exist at
all—these two behaviors cannot both be correct.

api/services/billing_service.py[266-297]
api/tests/unit_tests/services/test_billing_service.py[1297-1320]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`BillingService.get_plan_bulk()` is annotated/documented as returning `dict[str, SubscriptionPlan]` but currently inserts `None` for tenants whose payload fails validation. This violates the function contract and contradicts the newly added unit test that expects invalid tenants to be omitted.

### Issue Context
Keeping `None` values will also cascade into `get_plan_bulk_with_cache()` (which consumes `get_plan_bulk()`), potentially returning `None` to callers that expect a dict-like `SubscriptionPlan`.

### Fix Focus Areas
- api/services/billing_service.py[266-297]
- api/tests/unit_tests/services/test_billing_service.py[1297-1320]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Caches None plans 🐞 Bug ⛯ Reliability
Description
get_plan_bulk_with_cache caches and returns all values from get_plan_bulk without filtering,
including None entries. This can write "null" into Redis and propagate None to callers, causing
repeated cache misses or runtime errors when accessing plan fields.
Code

api/services/billing_service.py[R365-377]

+                for tenant_id, subscription_plan in bulk_plans.items():
+                    tenant_plans[tenant_id] = subscription_plan
+                    plans_to_cache[tenant_id] = subscription_plan
+
+                # Step 3: Batch update Redis cache using pipeline
+                if plans_to_cache:
+                    try:
+                        pipe = redis_client.pipeline()
+                        for tenant_id, subscription_plan in bulk_plans.items():
+                            redis_key = cls._make_plan_cache_key(tenant_id)
+                            # Serialize dict to JSON string
+                            json_str = json.dumps(subscription_plan)
+                            pipe.setex(redis_key, cls._PLAN_CACHE_TTL, json_str)
Evidence
Because get_plan_bulk can return None for a tenant, the cache layer will serialize and store that
None, and also return it in the resulting dict, violating the stated SubscriptionPlan mapping
contract.

api/services/billing_service.py[266-297]
api/services/billing_service.py[358-387]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`get_plan_bulk_with_cache()` currently assumes `get_plan_bulk()` returns valid `SubscriptionPlan` values, but the current implementation can include `None`. The cache path then serializes and stores these invalid values and returns them to callers.

### Issue Context
`json.dumps(None)` becomes `"null"`, which will be stored in Redis for the TTL window and will never validate back into `SubscriptionPlan`.

### Fix Focus Areas
- api/services/billing_service.py[358-387]
- api/services/billing_service.py[266-297]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. SubscriptionPlan.__str__ unnecessary override 📘 Rule violation ✓ Correctness
Description
SubscriptionPlan is a TypedDict (dict-typed structure) but the PR adds a __str__ method that
will not apply to the actual dict instances used at runtime. This is an unnecessary dunder override
and can mislead debugging/maintenance.
Code

api/services/billing_service.py[R28-31]

+    def __str__(self) -> str:
+        """Return a human-readable string representation for debugging."""
+        return f"Plan: {self['plan']}, Expiration: {self['expiration_date']}"
+
Evidence
The checklist disallows unnecessary dunder overrides, and the PR adds a __str__ override on a
TypedDict definition.

AGENTS.md
api/services/billing_service.py[28-31]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A `__str__` dunder override was added to a `TypedDict` (`SubscriptionPlan`). `TypedDict` is a typing construct and the actual objects used are plain `dict`s, so this override is misleading and effectively unused.

## Issue Context
The compliance rule restricts unnecessary dunder overrides.

## Fix Focus Areas
- api/services/billing_service.py[28-31]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. get_plan_bulk_with_cache raises Exception 📘 Rule violation ⛯ Reliability
Description
The new caching flow raises a generic Exception for a Redis mget length mismatch, which conflicts
with the requirement to use domain-specific exceptions in services. This reduces error clarity and
makes consistent controller-level translation harder.
Code

api/services/billing_service.py[R325-328]

+            if len(cached_values) != len(tenant_ids):
+                raise Exception(
+                    "get_plan_bulk_with_cache: unexpected error: redis mget failed: cached values length mismatch"
+                )
Evidence
The compliance rule requires domain-specific exceptions in services, but the PR introduces a `raise
Exception(...)` in the service caching logic.

AGENTS.md
api/services/billing_service.py[325-328]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`get_plan_bulk_with_cache` raises a generic `Exception` in service code, which violates the requirement to use domain-specific exceptions (or avoid raising for expected fallback paths).

## Issue Context
This error is used to trigger fallback behavior; using a generic exception reduces clarity and standardization.

## Fix Focus Areas
- api/services/billing_service.py[325-328]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. No pre-test Redis clean 🐞 Bug ⛯ Reliability
Description
The new integration test fixture states it cleans Redis before and after each test, but it only
deletes keys after the test yields. If stale keys with the same prefix exist before the suite
starts, the first cache-hit/miss test can be affected.
Code

api/tests/test_containers_integration_tests/services/test_billing_service.py[R22-33]

+    @pytest.fixture(autouse=True)
+    def setup_redis_cleanup(self, flask_app_with_containers):
+        """Clean up Redis cache before and after each test."""
+        with flask_app_with_containers.app_context():
+            # Clean up before test
+            yield
+            # Clean up after test
+            # Delete all test cache keys
+            pattern = f"{BillingService._PLAN_CACHE_KEY_PREFIX}*"
+            keys = redis_client.keys(pattern)
+            if keys:
+                redis_client.delete(*keys)
Evidence
The fixture contains only a comment for pre-cleanup and performs deletion exclusively in the
teardown section after yield.

api/tests/test_containers_integration_tests/services/test_billing_service.py[22-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The integration test fixture claims to clean Redis before/after, but currently only cleans after the test.

### Issue Context
This mainly impacts the first test executed if Redis contains leftover keys from earlier runs.

### Fix Focus Areas
- api/tests/test_containers_integration_tests/services/test_billing_service.py[22-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +28 to +31
def __str__(self) -> str:
"""Return a human-readable string representation for debugging."""
return f"Plan: {self['plan']}, Expiration: {self['expiration_date']}"

Choose a reason for hiding this comment

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

Remediation recommended

1. subscriptionplan.str unnecessary override 📘 Rule violation ✓ Correctness

SubscriptionPlan is a TypedDict (dict-typed structure) but the PR adds a __str__ method that
will not apply to the actual dict instances used at runtime. This is an unnecessary dunder override
and can mislead debugging/maintenance.
Agent Prompt
## Issue description
A `__str__` dunder override was added to a `TypedDict` (`SubscriptionPlan`). `TypedDict` is a typing construct and the actual objects used are plain `dict`s, so this override is misleading and effectively unused.

## Issue Context
The compliance rule restricts unnecessary dunder overrides.

## Fix Focus Areas
- api/services/billing_service.py[28-31]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +325 to +328
if len(cached_values) != len(tenant_ids):
raise Exception(
"get_plan_bulk_with_cache: unexpected error: redis mget failed: cached values length mismatch"
)

Choose a reason for hiding this comment

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

Remediation recommended

2. get_plan_bulk_with_cache raises exception 📘 Rule violation ⛯ Reliability

The new caching flow raises a generic Exception for a Redis mget length mismatch, which conflicts
with the requirement to use domain-specific exceptions in services. This reduces error clarity and
makes consistent controller-level translation harder.
Agent Prompt
## Issue description
`get_plan_bulk_with_cache` raises a generic `Exception` in service code, which violates the requirement to use domain-specific exceptions (or avoid raising for expected fallback paths).

## Issue Context
This error is used to trigger fallback behavior; using a generic exception reduces clarity and standardization.

## Fix Focus Areas
- api/services/billing_service.py[325-328]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +23 to +40
def setup_redis_cleanup(self, flask_app_with_containers):
"""Clean up Redis cache before and after each test."""
with flask_app_with_containers.app_context():
# Clean up before test
yield
# Clean up after test
# Delete all test cache keys
pattern = f"{BillingService._PLAN_CACHE_KEY_PREFIX}*"
keys = redis_client.keys(pattern)
if keys:
redis_client.delete(*keys)

def _create_test_plan_data(self, plan: str = "sandbox", expiration_date: int = 1735689600):
"""Helper to create test SubscriptionPlan data."""
return {"plan": plan, "expiration_date": expiration_date}

def _set_cache(self, tenant_id: str, plan_data: dict, ttl: int = 600):
"""Helper to set cache data in Redis."""

Choose a reason for hiding this comment

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

Action required

3. Integration tests missing annotations 📘 Rule violation ✓ Correctness

New test helper and test functions are missing required type annotations (parameters and/or return
types). This violates the rule requiring explicit type hints for all Python function definitions.
Agent Prompt
## Issue description
Several newly-added test functions/helpers lack required type annotations (parameters and return types).

## Issue Context
The repository requires explicit type annotations for all Python function definitions.

## Fix Focus Areas
- api/tests/test_containers_integration_tests/services/test_billing_service.py[23-24]
- api/tests/test_containers_integration_tests/services/test_billing_service.py[35-37]
- api/tests/test_containers_integration_tests/services/test_billing_service.py[39-43]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +285 to +292
try:
subscription_plan = subscription_adapter.validate_python(plan)
results[tenant_id] = subscription_plan
except Exception:
logger.exception(
"get_plan_bulk: failed to validate subscription plan for tenant(%s)", tenant_id
)
results[tenant_id] = None

Choose a reason for hiding this comment

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

Action required

4. None plan returned 🐞 Bug ✓ Correctness

BillingService.get_plan_bulk stores None for tenants whose plan payload fails validation even though
it declares returning dict[str, SubscriptionPlan]. This will either break callers that index into
the plan (e.g., plan['plan']) or fail the new unit test that expects invalid tenants to be omitted.
Agent Prompt
### Issue description
`BillingService.get_plan_bulk()` is annotated/documented as returning `dict[str, SubscriptionPlan]` but currently inserts `None` for tenants whose payload fails validation. This violates the function contract and contradicts the newly added unit test that expects invalid tenants to be omitted.

### Issue Context
Keeping `None` values will also cascade into `get_plan_bulk_with_cache()` (which consumes `get_plan_bulk()`), potentially returning `None` to callers that expect a dict-like `SubscriptionPlan`.

### Fix Focus Areas
- api/services/billing_service.py[266-297]
- api/tests/unit_tests/services/test_billing_service.py[1297-1320]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +365 to +377
for tenant_id, subscription_plan in bulk_plans.items():
tenant_plans[tenant_id] = subscription_plan
plans_to_cache[tenant_id] = subscription_plan

# Step 3: Batch update Redis cache using pipeline
if plans_to_cache:
try:
pipe = redis_client.pipeline()
for tenant_id, subscription_plan in bulk_plans.items():
redis_key = cls._make_plan_cache_key(tenant_id)
# Serialize dict to JSON string
json_str = json.dumps(subscription_plan)
pipe.setex(redis_key, cls._PLAN_CACHE_TTL, json_str)

Choose a reason for hiding this comment

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

Action required

5. Caches none plans 🐞 Bug ⛯ Reliability

get_plan_bulk_with_cache caches and returns all values from get_plan_bulk without filtering,
including None entries. This can write "null" into Redis and propagate None to callers, causing
repeated cache misses or runtime errors when accessing plan fields.
Agent Prompt
### Issue description
`get_plan_bulk_with_cache()` currently assumes `get_plan_bulk()` returns valid `SubscriptionPlan` values, but the current implementation can include `None`. The cache path then serializes and stores these invalid values and returns them to callers.

### Issue Context
`json.dumps(None)` becomes `"null"`, which will be stored in Redis for the TTL window and will never validate back into `SubscriptionPlan`.

### Fix Focus Areas
- api/services/billing_service.py[358-387]
- api/services/billing_service.py[266-297]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +22 to +33
@pytest.fixture(autouse=True)
def setup_redis_cleanup(self, flask_app_with_containers):
"""Clean up Redis cache before and after each test."""
with flask_app_with_containers.app_context():
# Clean up before test
yield
# Clean up after test
# Delete all test cache keys
pattern = f"{BillingService._PLAN_CACHE_KEY_PREFIX}*"
keys = redis_client.keys(pattern)
if keys:
redis_client.delete(*keys)

Choose a reason for hiding this comment

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

Remediation recommended

6. No pre-test redis clean 🐞 Bug ⛯ Reliability

The new integration test fixture states it cleans Redis before and after each test, but it only
deletes keys after the test yields. If stale keys with the same prefix exist before the suite
starts, the first cache-hit/miss test can be affected.
Agent Prompt
### Issue description
The integration test fixture claims to clean Redis before/after, but currently only cleans after the test.

### Issue Context
This mainly impacts the first test executed if Redis contains leftover keys from earlier runs.

### Fix Focus Areas
- api/tests/test_containers_integration_tests/services/test_billing_service.py[22-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants