Skip to content

Rgopalarao/ent 11683#131

Open
rgopalrao-sonata-png wants to merge 1 commit intomainfrom
rgopalarao/ENT-11683
Open

Rgopalarao/ent 11683#131
rgopalrao-sonata-png wants to merge 1 commit intomainfrom
rgopalarao/ENT-11683

Conversation

@rgopalrao-sonata-png
Copy link
Copy Markdown

@rgopalrao-sonata-png rgopalrao-sonata-png commented Apr 9, 2026

feat: Multi-License Subscription Support for Enterprise BFF

Tickets


Summary

This PR adds multi-license support for learner BFF flows behind enterprise_access.enable_multi_license_entitlements_bff, while preserving legacy behavior when the flag is OFF.

Problems We Resolved (Summary)

  • Previously, one license was effectively selected for learner flows, which produced incorrect entitlements for multi-catalog users.
  • The response did not provide catalog-to-license mapping for multi-license consumption.
  • Algolia scoping did not reflect all eligible activated catalog entitlements in multi-license mode.

API Contract Changes

  • Added licenses_by_catalog when enterprise_access.enable_multi_license_entitlements_bff is ON.
  • subscription_license selection now follows first-activation deterministic behavior (ENT-11672).
  • subscription_licenses continues to return the flat list for backward compatibility.
  • Algolia scoping now includes all activated licensed catalogs when flag is ON (legacy single-license scoping retained when OFF).

Example (flag ON)

{
  "subscription_license": { "...": "..." },
  "subscription_licenses": [{ "...": "..." }],
  "licenses_by_catalog": {
    "cat-uuid": [{ "...": "..." }]
  }
}

Example (flag OFF)

{
  "subscription_license": { "...": "..." },
  "subscription_licenses": [{ "...": "..." }]
}

Code Changes

Major files updated:

  • enterprise_access/apps/bffs/handlers.py
  • enterprise_access/apps/bffs/serializers.py
  • enterprise_access/apps/bffs/context.py
  • enterprise_access/toggles.py

Tests updated/added:

  • enterprise_access/apps/bffs/tests/test_multi_license.py
  • enterprise_access/apps/bffs/tests/test_handlers.py
  • enterprise_access/apps/bffs/tests/test_serializers.py
  • enterprise_access/apps/api/v1/tests/test_bff_views.py
  • enterprise_access/tests/test_toggles.py

Test Coverage

  • All new logic and edge cases are covered:
    • Single and multiple licenses
    • Catalog mapping
    • Feature flag ON/OFF
    • Tie-breakers, no-license, and regression scenarios
  • All tests pass (see CI for coverage %)

Code Changes

Modified Files

File Change
enterprise_access/apps/bffs/handlers.py Updated transform_subscriptions_result() to build licenses_by_catalog and apply first-activated selection rule when flag is ON; updated _scope_secured_algolia_by_flag() to scope across all catalog UUIDs
enterprise_access/apps/bffs/serializers.py Added licenses_by_catalog (DictField) to LearnerDashboardBFFResponseSerializer
enterprise_access/apps/bffs/context.py Updated BFF context to pass feature flag state through to handler methods
enterprise_access/toggles.py Added ENABLE_MULTI_LICENSE_ENTITLEMENTS_BFF WaffleFlag definition

New Test Files

File Purpose
enterprise_access/apps/bffs/tests/test_multi_license.py Dedicated multi-license test suite (all scenarios below)
enterprise_access/tests/test_toggles.py WaffleFlag unit tests (flag ON / flag OFF)

Changed Test Files

File Change
enterprise_access/apps/bffs/tests/test_handlers.py Added _subscriptions_result helper; updated TestScopeSecuredAlgoliaByFlag to cover multi-catalog scoping
enterprise_access/apps/api_client/tests/test_enterprise_catalog_client.py Updated assertions for multi-catalog client calls

Test Cases Covered

test_multi_license.pyTestTransformSubscriptionsResult

Test Description
test_flag_off_returns_v1_response When flag is OFF, response contains no licenses_by_catalog
test_flag_on_single_license Single license — v2 response, licenses_by_catalog has exactly one entry
test_flag_on_alice_three_licenses Alice (3 active licenses) — Leadership (activated 2024-01-15) is selected, not Compliance
test_flag_on_bob_four_licenses Bob (4 licenses: 2 activated, 1 assigned, 1 revoked) — only activated in licenses_by_catalog
test_flag_on_carol_five_licenses Carol (5 licenses across 5 catalogs) — all 5 appear in licenses_by_catalog
test_flag_on_dave_activation_flow Dave (1 activated + 2 assigned) — assigned licenses auto-activate, licenses_by_catalog reflects final state
test_flag_on_eve_tiebreaker Eve (2 licenses with same activation timestamp) — stable tiebreaker (e.g., UUID sort) applied
test_no_licenses Learner with no licenses — empty response, no errors

test_multi_license.pyTestScopeSecuredAlgoliaByFlag

Test Description
test_flag_off_uses_single_catalog Flag OFF — Algolia key scoped to single catalog UUID (legacy)
test_flag_on_scopes_all_catalogs Flag ON — Algolia key scoped across all catalog UUIDs from all active licenses
test_no_licenses_no_algolia_scope No licenses — Algolia scoping skipped gracefully

test_toggles.py

Test Description
test_flag_is_off_by_default WaffleFlag enable_multi_license_entitlements_bff is disabled by default
test_flag_can_be_enabled WaffleFlag can be enabled and handlers respond correctly

test_handlers.py (updated)

Test Description
test_scope_secured_algolia_single_license Single license — Algolia scoped to its catalog UUID
test_scope_secured_algolia_multi_license Multi-license — Algolia scoped to all catalog UUIDs

Test Learner Profiles (from test-data/)

User Licenses Notes
Alice (test-multi-alice@example.com) 3 activated (Leadership, Data Science, Compliance) Primary test persona; Leadership activated first (2024-01-15)
Bob (test-multi-bob@example.com) 4 total (2 activated, 1 assigned, 1 revoked) Tests status filtering
Carol (test-multi-carol@example.com) 5 activated across 5 catalogs Tests large license counts
Dave (test-multi-dave@example.com) 1 activated + 2 assigned Tests auto-activation flow
Eve (test-multi-eve@example.com) 3 licenses with same activation timestamp Tests tiebreaker stability

Test results:
In case of waffle flag off
flag_off
Response:
new_file_alice
In case of waffle flag on
waffleflag
alice_multilicense

@rgopalrao-sonata-png rgopalrao-sonata-png requested review from a team as code owners April 9, 2026 15:43
Copilot AI review requested due to automatic review settings April 9, 2026 15:43
@rgopalrao-sonata-png rgopalrao-sonata-png requested a review from a team as a code owner April 9, 2026 15:43
Copy link
Copy Markdown

Copilot AI left a comment

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 adds multi-license subscription support to the Enterprise BFF, including catalog-to-license indexing in the response and Algolia key scoping across all licensed catalogs behind a feature flag.

Changes:

  • Introduces license_schema_version and licenses_by_catalog in BFF subscription responses, gated by enterprise_access.enable_multi_license_entitlements_bff.
  • Updates license selection logic to use “first activated” tie-breaking (ENT-11672) and adds multi-license course-to-license mapping.
  • Adds support for scoping secured Algolia API keys by multiple catalog UUIDs, including caching and client updates, plus expanded test coverage.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
enterprise_access/toggles.py Defines the new Waffle flag for multi-license BFF behavior.
enterprise_access/tests/test_toggles.py Adds unit tests for the toggle helper.
enterprise_access/apps/bffs/handlers.py Implements multi-license processing, catalog indexing, selection rules, and Algolia scoping logic.
enterprise_access/apps/bffs/serializers.py Adds licenses_by_catalog and license_schema_version to the subscriptions serializer.
enterprise_access/apps/bffs/context.py Adds support for refreshing Algolia key metadata with an optional catalog scope.
enterprise_access/apps/bffs/api.py Extends Algolia key caching and downstream calls to include catalog_uuids scope.
enterprise_access/apps/api_client/enterprise_catalog_client.py Adds catalog_uuids query params support for the secured Algolia key endpoint.
enterprise_access/apps/api_client/tests/test_enterprise_catalog_client.py Updates/extends tests for enterprise-catalog client URL construction and scoped Algolia calls.
enterprise_access/apps/bffs/tests/test_multi_license.py Adds a dedicated test suite for multi-license selection, indexing, mapping, and flag behavior.
enterprise_access/apps/bffs/tests/test_handlers.py Adds/updates handler unit tests for selection rules and Algolia scoping.
enterprise_access/apps/bffs/tests/test_serializers.py Adds initial serializer validation tests for BFF serializers.
enterprise_access/apps/customer_billing/tests/test_tasks.py Removes reinstatement-email task tests and refactors some defaults dict literals.
enterprise_access/apps/customer_billing/tests/test_stripe_event_handlers.py Removes reinstatement-email behavior tests for subscription updated events.
enterprise_access/apps/customer_billing/tests/test_migrations.py Refactors ddt scenario inputs to dict(...) style.
enterprise_access/settings/base.py Adds rest_framework_swagger to installed apps; removes a Braze campaign setting.
enterprise_access/settings/test.py Removes a Braze campaign test setting.
enterprise_access/apps/api/tasks.py Adds an import (currently unused).
Comments suppressed due to low confidence (1)

enterprise_access/apps/api/tasks.py:10

  • requests is imported but not used anywhere in this module. Please remove the unused import to avoid lint/test failures.
import logging

import requests
from celery import shared_task
from django.conf import settings

from enterprise_access.apps.api.serializers import CouponCodeRequestSerializer, LicenseRequestSerializer

Copilot AI review requested due to automatic review settings April 9, 2026 16:51
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 12 changed files in this pull request and generated 9 comments.

Copilot AI review requested due to automatic review settings April 10, 2026 05:26
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 4 comments.

@rgopalrao-sonata-png rgopalrao-sonata-png force-pushed the rgopalarao/ENT-11683 branch 2 times, most recently from 081715c to 4801f8d Compare April 10, 2026 06:06
Copilot AI review requested due to automatic review settings April 10, 2026 06:06
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

Codecov Report

❌ Patch coverage is 98.38710% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.60%. Comparing base (6b6411c) to head (52ae1b9).

Files with missing lines Patch % Lines
enterprise_access/apps/bffs/context.py 95.31% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #131      +/-   ##
==========================================
+ Coverage   84.28%   84.60%   +0.31%     
==========================================
  Files         144      145       +1     
  Lines       12215    12347     +132     
  Branches     1163     1188      +25     
==========================================
+ Hits        10296    10446     +150     
+ Misses       1598     1586      -12     
+ Partials      321      315       -6     

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

Copilot AI review requested due to automatic review settings April 10, 2026 06:40
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.

Copilot AI review requested due to automatic review settings April 10, 2026 06:58
Copilot AI review requested due to automatic review settings April 10, 2026 11:08
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings April 10, 2026 11:47
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings April 11, 2026 06:29
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

@rgopalrao-sonata-png rgopalrao-sonata-png force-pushed the rgopalarao/ENT-11683 branch 2 times, most recently from 9264667 to 0960776 Compare April 13, 2026 05:15
Copilot AI review requested due to automatic review settings April 13, 2026 05:15
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 15 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Contributor

@vshaikismail-sonata vshaikismail-sonata left a comment

Choose a reason for hiding this comment

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

Looks good, Overall validations and logging are well handled.

Copilot AI review requested due to automatic review settings April 13, 2026 09:26
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

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

else:
digest = hashlib.sha256(
','.join(sorted(str(u) for u in catalog_uuids)).encode()
).hexdigest()[:16]
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

secured_algolia_api_key_cache_key truncates the SHA-256 digest to 16 hex chars ([:16]), which introduces a (small but real) risk of cache-key collisions between different catalog UUID sets. Since versioned_cache_key() already hashes the final key to a fixed-length digest (enterprise_access/cache_utils.py:15-26), there’s no cache-backend key-length constraint here; consider using the full hexdigest (or avoid the extra hashing entirely if input size isn’t a concern) to eliminate collision risk while still keeping the cache key bounded.

Suggested change
).hexdigest()[:16]
).hexdigest()

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is a good suggestion, versioned_cache_key() just hashes the entire set of strings you throw into it.

context, error_response, error_status = self._create_context(
request, context_class, context_kwargs=context_kwargs,
)
if context is None:
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

After creating the context, consider short-circuiting when it already contains errors / an error status_code (e.g., invalid enterprise_customer_uuid causes HandlerContext to return early with a 400). Currently the handler will still run and may trigger unnecessary downstream calls or add secondary errors due to missing context data.

Suggested change
if context is None:
if context is None or error_status is not None:

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also a good suggestion

@iloveagent57 iloveagent57 self-assigned this Apr 14, 2026
Copy link
Copy Markdown
Member

@iloveagent57 iloveagent57 left a comment

Choose a reason for hiding this comment

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

Approach looks good in general, here's some feedback mostly around the algolia key thing.

else:
digest = hashlib.sha256(
','.join(sorted(str(u) for u in catalog_uuids)).encode()
).hexdigest()[:16]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is a good suggestion, versioned_cache_key() just hashes the entire set of strings you throw into it.

context, error_response, error_status = self._create_context(
request, context_class, context_kwargs=context_kwargs,
)
if context is None:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this is also a good suggestion

'developer_message' (str): Message of corresponding error indicating an actionable developer message
"""
response = self.get(self.secured_algolia_api_key_endpoint(enterprise_customer_uuid))
query_params = {'catalog_uuids': catalog_uuids} if catalog_uuids is not None else None
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

ref: https://github.com/edx/enterprise-catalog/blob/3f600ae3e8879a9a30ce118eb730a5c5d433b8f6/enterprise_catalog/apps/api/v1/views/enterprise_customer.py#L248
I think we can actually get rid of this query params, because the endpoint already scopes to all available catalog queries by default (with no method for overriding).

Comment on lines +405 to +406
# Preserve main's flat list behavior and expose all licenses.
response_subscription_licenses = subscription_licenses
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't understand what these two lines are meant to do.

Comment on lines +341 to +345
# Note: secured Algolia key initialization happens during context setup via
# _initialize_secured_algolia_api_keys() (unless deferred). Handlers may still call
# refresh_secured_algolia_api_keys() later, once relevant catalog UUIDs are known,
# to perform scoped/catalog-specific refreshes. Do not assume that later refresh
# has already happened here.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: this can be added to the docstring for your helper method below

"""
self.refresh_secured_algolia_api_keys()

def refresh_secured_algolia_api_keys(self, catalog_uuids=None):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may not need this at all if the enterprise-catalog algolia key endpoint already scopes the key to all available catalogs for the customer (and provides no means of override with query params).

Comment on lines +25 to +32
class SubscriptionLicenseProcessor:
"""
Handles subscription license data transformation.
Preserves collection semantics while maintaining backward compatibility.

This processor supports multi-license scenarios where a learner may have
access to multiple subscription licenses across different catalogs.
"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍 I like how you modeled this class.

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.

4 participants