Skip to content

Commit

Permalink
feat: process expired licenses
Browse files Browse the repository at this point in the history
  • Loading branch information
muhammad-ammar committed Oct 31, 2024
1 parent 03ad938 commit d15a9c8
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 0 deletions.
8 changes: 8 additions & 0 deletions license_manager/apps/api_client/enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class EnterpriseApiClient(BaseOAuthClient):
course_enrollments_revoke_endpoint = api_base_url + 'licensed-enterprise-course-enrollment/license_revoke/'
bulk_licensed_enrollments_expiration_endpoint = api_base_url \
+ 'licensed-enterprise-course-enrollment/bulk_licensed_enrollments_expiration/'
unlink_users_endpoint = api_base_url + 'enterprise-customer/unlink_users/'

def get_enterprise_customer_data(self, enterprise_customer_uuid):
"""
Expand Down Expand Up @@ -189,3 +190,10 @@ def bulk_enroll_enterprise_learners(self, enterprise_id, options):
"""
enrollment_url = '{}{}/enroll_learners_in_courses/'.format(self.enterprise_customer_endpoint, enterprise_id)
return self.client.post(enrollment_url, json=options, timeout=settings.BULK_ENROLL_REQUEST_TIMEOUT_SECONDS)

def bulk_unlink_enterprise_users(self, enterprise_uuid, options):
"""
Calls the Enterprise `unlink_users` API to unlink learners for an enterprise.
"""
enrollment_url = '{}{}/unlink_users/'.format(self.unlink_users_endpoint, enterprise_uuid)
return self.client.post(enrollment_url, json=options, timeout=settings.BULK_ENROLL_REQUEST_TIMEOUT_SECONDS)
2 changes: 2 additions & 0 deletions license_manager/apps/subscriptions/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,5 @@ class SegmentEvents:
}

ENTERPRISE_BRAZE_ALIAS_LABEL = 'Enterprise' # Do Not change this, this is consistent with other uses across edX repos.

EXPIRED_LICENSE_PROCESSED = 'edx.server.license-manager.expired.license.processed'
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@

import logging

from django.conf import settings
from django.core.management.base import BaseCommand
from django.core.paginator import Paginator
from django.db.models import Exists, OuterRef

from license_manager.apps.api_client.enterprise import EnterpriseApiClient
from license_manager.apps.subscriptions.constants import (
ACTIVATED,
ASSIGNED,
EXPIRED_LICENSE_PROCESSED,
)
from license_manager.apps.subscriptions.models import (
CustomerAgreement,
License,
LicenseEvent,
SubscriptionPlan,
)
from license_manager.apps.subscriptions.utils import localized_utcnow


logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = (
'Process expired licenses.'
)

def add_arguments(self, parser):
"""
Entry point to add arguments.
"""
parser.add_argument(
'--dry-run',
action='store_true',
dest='dry_run',
default=False,
help='Dry Run, print log messages without firing the segment event.',
)

def expired_licenses(self, enterprise_customer_uuid):
"""
Get expired licenses.
"""
now = localized_utcnow()
expired_subscription_plan_uuids = []

customer_agreement = CustomerAgreement.objects.get(enterprise_customer_uuid=enterprise_customer_uuid)

expired_subscription_plans = list(
SubscriptionPlan.objects.filter(
customer_agreement=customer_agreement,
expiration_date__lt=now,
).select_related(
'customer_agreement'
).prefetch_related(
'licenses'
)
)

for expired_subscription_plan in expired_subscription_plans:
# exclude subscription plan if there is a renewal
if expired_subscription_plan.get_renewal():
continue

expired_subscription_plan_uuids.append(expired_subscription_plan.uuid)

# include prior renewals
for prior_renewal in expired_subscription_plan.prior_renewals:
expired_subscription_plan_uuids.append(prior_renewal.prior_subscription_plan.uuid)

queryset = License.objects.filter(
status__in=[ASSIGNED, ACTIVATED],
renewed_to=None,
subscription_plan__uuid__in=expired_subscription_plan_uuids,
).select_related(
'subscription_plan',
).values('uuid', 'lms_user_id', 'user_email')

# Subquery to check for the existence of `EXPIRED_LICENSE_PROCESSED`
event_exists_subquery = LicenseEvent.objects.filter(
license=OuterRef('pk'),
event_name=EXPIRED_LICENSE_PROCESSED
).values('pk')

# Exclude previously processed licenses.
queryset = queryset.exclude(Exists(event_exists_subquery))

return queryset

def handle(self, *args, **options):
"""
Process expired licenses and un-link learners.
"""
unlink = not options['dry_run']

log_prefix = '[PROCESS_EXPIRED_LICENSES]'
if not unlink:
log_prefix = '[DRY RUN]'

logger.info('%s Command started.', log_prefix)

enterprise_customer_uuids = settings.CUSTOMERS_WITH_EXPIRED_LICENSES_UNLINKING_ENABLED
for enterprise_customer_uuid in enterprise_customer_uuids:
logger.info('%s Processing started for licenses. Enterprise: [%s]', log_prefix, enterprise_customer_uuid)
self.process_expired_licenses(enterprise_customer_uuid, log_prefix, unlink)
logger.info('%s Processing completed for licenses. Enterprise: [%s]', log_prefix, enterprise_customer_uuid)

logger.info('%s Command completed.', log_prefix)

def process_expired_licenses(self, enterprise_customer_uuid, log_prefix, unlink):
"""
Process expired licenses and un-link learners.
"""
expired_licenses = self.expired_licenses(enterprise_customer_uuid)

if not expired_licenses:
logger.info(
'%s No expired licenses were found for enterprise: [%s].',
log_prefix, enterprise_customer_uuid
)
return

paginator = Paginator(expired_licenses, 100)
for page_number in paginator.page_range:
licenses = paginator.page(page_number)

license_uuids = [license.get('uuid') for license in licenses]
user_emails = [license.get('user_email') for license in licenses]

logger.info(
"%s learners unlinked for licenses. Enterprise: [%s], LicenseUUIDs: [%s].",
log_prefix,
enterprise_customer_uuid,
license_uuids
)

if unlink:
# EnterpriseApiClient.bulk_unlink_enterprise_users(
# enterprise_customer_uuid,
# {
# 'user_emails': user_emails,
# 'is_relinkable': False
# },

# )

# Create license events for unlinked licenses to avoid processing them again.
unlinked_license_events = [
LicenseEvent(license_id=license_uuid, event_name=EXPIRED_LICENSE_PROCESSED)
for license_uuid in license_uuids
]
LicenseEvent.objects.bulk_create(unlinked_license_events, batch_size=100)
1 change: 1 addition & 0 deletions license_manager/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,4 @@
]

CUSTOMERS_WITH_CUSTOM_LICENSE_EVENTS = ['00000000-1111-2222-3333-444444444444']
CUSTOMERS_WITH_EXPIRED_LICENSES_UNLINKING_ENABLED = ['00000000-1111-2222-3333-444444444444']

0 comments on commit d15a9c8

Please sign in to comment.