Skip to content

feat: retrieve list of academy info#194

Open
tsunkara-sonata wants to merge 7 commits into
mainfrom
tsunkara/ENT-11468
Open

feat: retrieve list of academy info#194
tsunkara-sonata wants to merge 7 commits into
mainfrom
tsunkara/ENT-11468

Conversation

@tsunkara-sonata

@tsunkara-sonata tsunkara-sonata commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

https://2u-internal.atlassian.net/browse/ENT-11468

Description:
To ensure the centralized Academy information stored in edX Enterprise is accessible to the frontend checkout flow and other internal services, we need to implement a RESTful API.

ssp-product-lookup

Copilot AI review requested due to automatic review settings June 9, 2026 08:34
@tsunkara-sonata tsunkara-sonata requested review from a team as code owners June 9, 2026 08:34

Copilot AI 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.

Pull request overview

This PR introduces a public, read-only REST API for listing and retrieving “academy-backed” SSP products (to support frontend checkout and internal consumers), enriching the SSP product model with additional academy metadata fields and updating the enterprise-catalog client’s academies endpoint behavior.

Changes:

  • Added /api/v1/ssp-products/ public endpoints (list/retrieve) with optional Stripe-backed pricing and scoped throttling.
  • Extended SspProduct academy metadata properties (long name, description fallbacks, thumbnail field preference) and added/expanded unit tests.
  • Updated enterprise-catalog API client/tests to fetch Academies from the v1 endpoint.

Reviewed changes

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

Show a summary per file
File Description
enterprise_access/settings/base.py Adds a new DRF throttle scope/rate for the public SSP products endpoint.
enterprise_access/apps/customer_billing/tests/test_models.py Expands SspProduct academy-property tests and adds fallback-logic coverage.
enterprise_access/apps/customer_billing/models.py Enhances academy metadata properties (long name/description/thumbnail selection).
enterprise_access/apps/api/v1/views/customer_billing.py Implements SspProductViewSet public list/retrieve with Stripe pricing lookup + thumbnail URL building.
enterprise_access/apps/api/v1/views/init.py Exposes the new SspProductViewSet via the views package.
enterprise_access/apps/api/v1/urls.py Registers the ssp-products router endpoint under the customer billing API flag.
enterprise_access/apps/api/v1/tests/test_customer_billing_ssp_products.py Adds comprehensive endpoint tests (pricing behavior, fallbacks, anonymous access).
enterprise_access/apps/api/serializers/customer_billing.py Adds response serializer(s) for SSP essentials product payloads.
enterprise_access/apps/api/serializers/init.py Re-exports the new SSP serializers.
enterprise_access/apps/api_client/tests/test_enterprise_catalog_client.py Updates academy fetch test to expect the v1 academies endpoint.
enterprise_access/apps/api_client/enterprise_catalog_client.py Points get_academy calls to the enterprise-catalog v1 academies endpoint.

Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment on lines +203 to +206
return {
'unit_amount_decimal': Decimal(price.unit_amount or 0) / 100,
'stripe_name': stripe_product_name,
'stripe_description': stripe_product_description,
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/customer_billing/tests/test_models.py
@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.39%. Comparing base (b7453ee) to head (4f0717a).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #194      +/-   ##
==========================================
+ Coverage   86.25%   86.39%   +0.13%     
==========================================
  Files         153      153              
  Lines       12660    12772     +112     
  Branches     1211     1224      +13     
==========================================
+ Hits        10920    11034     +114     
+ Misses       1425     1424       -1     
+ Partials      315      314       -1     

☔ 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.

Copilot AI review requested due to automatic review settings June 9, 2026 10:19

Copilot AI 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.

Pull request overview

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

Comment thread enterprise_access/apps/api/v1/tests/test_customer_billing_ssp_products.py Outdated
Comment thread enterprise_access/apps/api/serializers/customer_billing.py Outdated
Comment on lines +146 to +173
# Last-resort fallback: scan active prices with auto-pagination to capture keys missed by filtered lookups.
if still_missing_lookup_keys:
try:
all_active_prices = stripe.Price.list(
active=True,
expand=['data.product'],
limit=100,
)
requested_slugs = set((slug_by_lookup_key or {}).values())
missing_lookup_keys = set(still_missing_lookup_keys)
resolved_lookup_keys = set()
resolved_slugs = set()

for stripe_price in all_active_prices.auto_paging_iter():
lookup_key = getattr(stripe_price, 'lookup_key', None)
stripe_metadata = getattr(stripe_price, 'metadata', None) or {}
stripe_product_slug = self._metadata_value(stripe_metadata, 'ssp_product_slug')
lookup_key_matches = lookup_key in missing_lookup_keys
slug_matches = stripe_product_slug in requested_slugs
if lookup_key_matches or slug_matches:
prices.append(stripe_price)
if lookup_key_matches:
resolved_lookup_keys.add(lookup_key)
if slug_matches:
resolved_slugs.add(stripe_product_slug)

if resolved_lookup_keys >= missing_lookup_keys and resolved_slugs >= requested_slugs:
break
Comment thread enterprise_access/apps/api/serializers/customer_billing.py Outdated
Comment thread enterprise_access/apps/api_client/enterprise_catalog_client.py
Copilot AI review requested due to automatic review settings June 10, 2026 09:43

Copilot AI 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.

Pull request overview

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

Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment on lines +146 to +153
# Last-resort fallback: scan active prices with auto-pagination to capture keys missed by filtered lookups.
if still_missing_lookup_keys:
try:
all_active_prices = stripe.Price.list(
active=True,
expand=['data.product'],
limit=100,
)
Comment thread enterprise_access/apps/api/serializers/__init__.py Outdated
Comment thread enterprise_access/apps/api/v1/tests/test_customer_billing_ssp_products.py Outdated
Comment on lines +545 to +546
detail_url = reverse('api:v1:ssp-products-detail', kwargs={'slug': 'public-mapped-slug'})
response = self.client.get(detail_url)
Comment on lines +594 to +615
def test_chunk_values_and_metadata_value_and_payment_helpers():
# Test chunking
vals = list(range(7))
chunks = list(SspProductViewSet._chunk_values(vals, 3))
assert chunks == [vals[0:3], vals[3:6], vals[6:7]]

# metadata_value: non-dict object that raises KeyError/TypeError
class BadMeta:
"""A metadata-like object that raises on lookup to exercise error handling."""
def __getitem__(self, key):
raise KeyError()

assert SspProductViewSet._metadata_value({'a': 1}, 'a') == 1
assert SspProductViewSet._metadata_value(BadMeta(), 'a') is None

# Payment method status mapping

assert BillingManagementViewSet._get_payment_method_status({'type': 'card'}) == 'verified'
assert BillingManagementViewSet._get_payment_method_status({
'type': 'us_bank_account',
'us_bank_account': {'status_details': {'status': 'verified'}}
}) == 'verified'
Comment thread enterprise_access/apps/api/v1/tests/test_customer_billing_ssp_products.py Outdated
Copilot AI review requested due to automatic review settings June 11, 2026 03:59

Copilot AI 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.

Pull request overview

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

Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/api/v1/tests/test_customer_billing_ssp_products.py Outdated
Comment thread enterprise_access/apps/api/v1/tests/test_customer_billing_ssp_products.py Outdated
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Copilot AI review requested due to automatic review settings June 11, 2026 05:33

Copilot AI 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.

Pull request overview

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

Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
self.api_base_url = urljoin(settings.ENTERPRISE_CATALOG_URL, f'api/{self.api_version}/')
self.academies_endpoint = urljoin(self.api_base_url, 'academies/')
# Academies are exposed on v1 of the enterprise-catalog API, not v2.
self.academies_endpoint = urljoin(settings.ENTERPRISE_CATALOG_URL, 'api/v1/academies/')
Comment thread enterprise_access/apps/api/serializers/customer_billing.py Outdated
@rthota-sonata-hue

rthota-sonata-hue commented Jun 11, 2026

Copy link
Copy Markdown

Looks good to me. Please squash to single commit.

@marlonkeating marlonkeating self-assigned this Jun 11, 2026
Comment thread enterprise_access/apps/api/serializers/customer_billing.py Outdated
Comment thread enterprise_access/apps/api_client/enterprise_catalog_client.py
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py
Comment thread enterprise_access/apps/customer_billing/tests/test_models.py
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Copilot AI review requested due to automatic review settings June 11, 2026 18:20

Copilot AI 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.

Pull request overview

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

Comment thread enterprise_access/apps/api/v1/tests/test_customer_billing_ssp_products.py Outdated
Comment thread enterprise_access/apps/api/v1/tests/test_customer_billing_ssp_products.py Outdated
Comment on lines +899 to +906
def test_normalize_invoice_status_and_yearly_amount_and_license_count(monkeypatch):

# normalize invoice status
assert BillingManagementViewSet._normalize_invoice_status('paid') == 'paid'
assert BillingManagementViewSet._normalize_invoice_status('open') == 'open'
assert BillingManagementViewSet._normalize_invoice_status('void') == 'void'
assert BillingManagementViewSet._normalize_invoice_status('uncollectible') == 'uncollectible'
assert BillingManagementViewSet._normalize_invoice_status('weird') == 'open'
Comment on lines +462 to +466
cache_key = versioned_cache_key('all_stripe_prices')
cached_response = TieredCache.get_cached_response(cache_key)
if cached_response.is_found:
all_prices = get_all_stripe_prices()
used_cache = True
Comment thread enterprise_access/apps/api/serializers/customer_billing.py Outdated
Comment thread enterprise_access/apps/api_client/enterprise_catalog_client.py
Comment on lines +151 to +158
# Last-resort fallback: scan active prices with auto-pagination to capture keys missed by filtered lookups.
if still_missing_lookup_keys:
try:
all_active_prices = stripe.Price.list(
active=True,
expand=['data.product'],
limit=100,
)
Copilot AI review requested due to automatic review settings June 11, 2026 19:37
except (KeyError, TypeError): # pragma: no cover - metadata type varies by Stripe SDK object wrappers
return None

def _get_pricing_by_lookup_key(self, lookup_keys, slug_by_lookup_key=None): # pylint: disable=too-many-statements

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.

# pylint: disable=too-many-statements we generally don't allow disabling this pylint warning. This function is indeed too long/complex. Also, this logic is very duplicative with code that already exists: https://github.com/edx/enterprise-access/blob/main/enterprise_access/apps/customer_billing/pricing_api.py#L253 Try to make use of that code, doing light refactoring as needed.

Comment thread enterprise_access/apps/customer_billing/models.py Outdated
Comment thread enterprise_access/apps/customer_billing/models.py Outdated

Copilot AI 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.

Pull request overview

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

Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Comment thread enterprise_access/apps/api_client/enterprise_catalog_client.py
Comment on lines +931 to +935
def test_normalize_invoice_status_and_yearly_amount_and_license_count(monkeypatch):

# normalize invoice status
assert BillingManagementViewSet._normalize_invoice_status('paid') == 'paid'
assert BillingManagementViewSet._normalize_invoice_status('open') == 'open'
Copilot AI review requested due to automatic review settings June 11, 2026 20:44

Copilot AI 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.

Pull request overview

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

Comment thread enterprise_access/apps/api/serializers/customer_billing.py Outdated
Comment on lines 21 to +23
self.api_base_url = urljoin(settings.ENTERPRISE_CATALOG_URL, f'api/{self.api_version}/')
self.academies_endpoint = urljoin(self.api_base_url, 'academies/')
# Academies are exposed on v1 of the enterprise-catalog API, not v2.
self.academies_endpoint = urljoin(settings.ENTERPRISE_CATALOG_URL, 'api/v1/academies/')
Comment thread enterprise_access/apps/api/v1/views/customer_billing.py Outdated
Copilot AI review requested due to automatic review settings June 12, 2026 07:23

Copilot AI 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.

Pull request overview

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

Comment on lines +129 to +133
long_name = self._academy_data.get('long_name')
if long_name:
return long_name

return self._academy_data.get('long_name') or self.academy_title
Comment on lines +476 to +478
cond_is_str = isinstance(requested_slug, str)
cond_dash_only = ('-' in requested_slug and '_' not in requested_slug)
is_public_slug = cond_is_str and cond_dash_only # pragma: no cover - heuristic
Comment on lines +485 to +489
# First, check whether the requested slug actually matches a
# database `stripe_price_lookup_key`. This allows direct lookup_key
# URLs (where slug==lookup_key) to resolve without calling Stripe.
db_qs = self.get_queryset().filter(stripe_price_lookup_key=requested_slug) # pragma: no cover - db
product = db_qs.first()
Comment on lines +119 to +121
unit_amount_decimal = Decimal(
price.unit_amount or 0,
) / 100 # pragma: no cover - stripe price payload extraction
Comment thread enterprise_access/apps/api_client/enterprise_catalog_client.py
@iloveagent57

Copy link
Copy Markdown
Member

Two structural concerns worth addressing before merge

Pricing lookup: reinventing what pricing_api.py already does

pricing_api.py already has exactly what this view needs:

  • get_all_stripe_prices() — fetches all active prices via auto_paging_iter(), serializes them keyed by lookup_key, and caches the result. It's 50 lines with a well-defined SerializedPriceData TypedDict.
  • get_ssp_product_pricing() — builds on get_all_stripe_prices() to return pricing indexed by product key.

The PR introduces _get_pricing_by_lookup_key() — 145 lines, three Stripe fallback tiers — and then separately adds its own auto_paging_iter() full scan in retrieve(). Both duplicate what get_all_stripe_prices() already does, without the caching.

The simple version of list() pricing:

# get_all_stripe_prices() is already cached
all_prices = get_all_stripe_prices()  # {lookup_key: SerializedPriceData}
pricing_by_lookup_key = {
    p.stripe_price_lookup_key: all_prices.get(p.stripe_price_lookup_key)
    for p in products
}

No chunking, no fallback tiers, no auto_paging_iter() in the view. The only real difference is that get_all_stripe_prices() raises StripePricingError on validation failures where the new code degrades gracefully — but that's a one-line try/except fix, not a reason for 145 lines of custom Stripe orchestration.

The retrieve() slug-to-lookup_key resolution is legitimately harder, but even there the view already imports get_all_stripe_prices() for the cache path. The fallback chain should just call that function and use _lookup_key_from_cached_prices(), rather than duplicating the full-scan logic inline.


_serialize_product(): the serializer should own this

Every other viewset in this file (CheckoutIntentViewSet, BillingManagementViewSet, StripeEventSummaryViewSet) uses the standard DRF pattern: get_serializer_class() + get_serializer() + serializer.data. The serializer owns field-level logic; the view orchestrates calls.

Here the pattern is inverted. _serialize_product() builds a plain dict with all the field priority logic (academy vs. Stripe fallbacks, price formatting, thumbnail URL joining), then hands that dict to get_serializer() which just validates and formats it. SspEssentialsProductResponseSerializer doesn't know about SspProduct at all — it's purely a schema enforcer on a dict the view already assembled.

The right approach is to move the field logic into the serializer:

class SspEssentialsProductResponseSerializer(serializers.Serializer):
    name = serializers.SerializerMethodField()
    price = serializers.SerializerMethodField()
    # ...

    def get_name(self, obj):
        # obj is SspProduct; self.context['pricing'] has price data
        return obj.academy_title or self.context['pricing'].get(obj.stripe_price_lookup_key, {}).get('stripe_name')

    def get_price(self, obj):
        price_data = self.context['pricing'].get(obj.stripe_price_lookup_key, {})
        raw = price_data.get('unit_amount_decimal')
        if raw is None:
            return None
        try:
            return f'{Decimal(str(raw)):.2f}'
        except (InvalidOperation, TypeError, ValueError):
            return None

Then the view becomes:

def list(self, request, *args, **kwargs):
    products = list(self.get_queryset())
    pricing = get_all_stripe_prices() if self._include_pricing() else {}
    serializer = self.get_serializer(products, many=True, context={'pricing': pricing, 'request': request})
    return Response(serializer.data)

This is the pattern the rest of the file uses. The view shouldn't be building dicts that get immediately re-serialized — that's what serializers are for.


Bottom line: replacing _get_pricing_by_lookup_key() with a call to the existing cached get_all_stripe_prices(), and moving the field assembly from _serialize_product() into the serializer via SerializerMethodField + context, would collapse roughly 200 lines of view code into ~30 and align with every other viewset in this file.

@iloveagent57 iloveagent57 self-assigned this Jun 15, 2026
Comment on lines +246 to +299
except (exceptions.NotFound, Http404) as exc:
requested_slug = kwargs.get('slug')
# Preserve prior behavior for obvious public slugs (e.g., 'teams-yearly')
# which should return 404 without attempting Stripe resolution. Heuristic:
# treat values containing '-' and not '_' as public slugs and re-raise.
cond_is_str = isinstance(requested_slug, str)
cond_dash_only = ('-' in requested_slug and '_' not in requested_slug)
is_public_slug = cond_is_str and cond_dash_only
if is_public_slug:
raise Http404() from exc

product = None
lookup_key_candidate = None
used_cache = False

# First, check whether the requested slug actually matches a DB stored lookup_key
db_qs = self.get_queryset().filter(stripe_price_lookup_key=requested_slug)
product = db_qs.first()
logger.debug('DB lookup for stripe_price_lookup_key=%s returned %s', requested_slug, bool(product))

# If we found a DB product by lookup_key, skip Stripe resolution.
if not product:
# Try direct lookup_key match in Stripe (active prices first).
product_from_price, lookup_key_from_price = self._stripe_single_lookup(requested_slug, active=True)
if product_from_price:
product = product_from_price
if lookup_key_from_price:
lookup_key_candidate = lookup_key_from_price

# If the active lookup did not return a result, try a non-active single lookup.
if not (lookup_key_candidate or product):
product_from_price, lookup_key_from_price = self._stripe_single_lookup(requested_slug, active=False)
if product_from_price:
product = product_from_price
if lookup_key_from_price:
lookup_key_candidate = lookup_key_from_price

# If still nothing found, consult the cached all-prices mapping
# only if it exists to avoid triggering a live full Stripe scan.
if not (lookup_key_candidate or product):
cached_lookup, cache_used = self._consult_cached_mapping(requested_slug)
if cache_used:
used_cache = True
lookup_key_candidate = cached_lookup

# If direct lookup_key match didn't yield a product, do a
# full active-price scan and search for Stripe metadata that
# references the requested slug (metadata.ssp_product_slug).
if not product and not lookup_key_candidate and not used_cache:
lookup_key_candidate = self._scan_active_prices_for_slug(requested_slug)

if not product and lookup_key_candidate:
product = self.get_queryset().filter(
stripe_price_lookup_key=lookup_key_candidate,

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 too much code that's far too nested just to help a caller using the wrong value. just use default DRF retrieval behavior and skip all of this.

Comment on lines +146 to +218
def _lookup_key_from_cached_prices(requested_slug, all_prices):
"""Resolve lookup_key from cached Stripe price map using metadata then fuzzy match."""
if requested_slug in all_prices:
return requested_slug

for lookup_key, price_data in all_prices.items():
meta = (price_data.get('product') or {}).get('metadata') or {}
if meta.get('ssp_product_slug') == requested_slug:
return lookup_key

for lookup_key in all_prices.keys():
if requested_slug in lookup_key or lookup_key in requested_slug:
return lookup_key

return None

def _stripe_single_lookup(self, requested_slug, active=True):
"""Attempt a single-lookup Key query against Stripe and resolve product/lookup_key.

Returns a tuple `(product_or_none, lookup_key_or_none)`.
"""
try:
if active:
price_resp = stripe.Price.list(
lookup_keys=[requested_slug],
active=True,
expand=['data.product'],
limit=1,
)
else:
price_resp = stripe.Price.list(
lookup_keys=[requested_slug],
expand=['data.product'],
limit=1,
)
except stripe.error.StripeError:
return None, None

return self._resolve_product_from_price_response(price_resp)

def _consult_cached_mapping(self, requested_slug):
"""If the cached all-prices map exists, consult it for a lookup_key.

Returns a tuple `(lookup_key_or_none, used_cache_bool)`.
"""
try:
cache_key = versioned_cache_key('all_stripe_prices')
cached_response = TieredCache.get_cached_response(cache_key)
if cached_response.is_found:
all_prices = get_all_stripe_prices()
lookup_key = self._lookup_key_from_cached_prices(requested_slug, all_prices)
return lookup_key, True
except StripePricingError:
return None, False
return None, False

def _scan_active_prices_for_slug(self, requested_slug):
"""Run a full active-price scan against Stripe and return a matching lookup_key if found."""
try:
all_active = stripe.Price.list(active=True, expand=['data.product'], limit=100)
for price in all_active.auto_paging_iter():
meta = getattr(price, 'metadata', None) or {}
meta_slug = self._metadata_value(meta, 'ssp_product_slug')
lookup_key_val = getattr(price, 'lookup_key', None)

if meta_slug == requested_slug:
return lookup_key_val

if lookup_key_val and (requested_slug in lookup_key_val or lookup_key_val in requested_slug):
return lookup_key_val
except stripe.error.StripeError:
return None
return 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.

none of this seems necessary, the SspProduct model maps the stripe lookup key to a product slug

@marlonkeating marlonkeating dismissed their stale review June 16, 2026 14:08

Dismissing review to unblock since I will be out of the office starting tomorrow.

assert BillingManagementViewSet._get_license_count(sub) == 5


def test_normalize_invoice_status_and_yearly_amount_and_license_count(monkeypatch):

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 really understand why we need this - is it used anywhere?

Comment on lines +35 to +54
self.essentials_product = SspProduct.objects.create(
slug='ai-academy-yearly',
stripe_price_lookup_key='ai_academy_yearly_price',
academy_uuid=uuid.uuid4(),
catalog_query_uuid=uuid.uuid4(),
license_manager_product_id_trial=2,
license_manager_product_id_paid=1,
is_active=True,
)
self.teams_product, _ = SspProduct.objects.get_or_create(
slug='teams-yearly',
defaults={
'stripe_price_lookup_key': 'teams_subscription_license_yearly',
'academy_uuid': None,
'catalog_query_uuid': uuid.uuid4(),
'license_manager_product_id_trial': 2,
'license_manager_product_id_paid': 1,
'is_active': True,
},
)

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.

if the tests don't ever modify these objects, its more efficient to define them inside of setUpClass(). that way, they'll only be created once, instead of once per test function.

@override_settings(SSP_ESSENTIALS_THUMBNAIL_S3_BASE_URL='https://s3.amazonaws.com/essentials-bucket')
@mock.patch('enterprise_access.apps.api.v1.views.customer_billing.get_all_stripe_prices')
@mock.patch('enterprise_access.apps.customer_billing.models.get_cached_academy_data')
def test_list_ssp_products_success(self, mock_get_cached_academy_data, mock_get_all_stripe_prices):

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 could probably compact these tests quite a bit by using https://ddt.readthedocs.io/en/latest/
look for usage examples in this repo, e.g.

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.

5 participants