Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: process expired licenses and unlink enterprise learners #733

Merged
merged 1 commit into from
Nov 6, 2024

Conversation

muhammad-ammar
Copy link
Contributor

@muhammad-ammar muhammad-ammar commented Oct 31, 2024

Description: Add a management command to iterate through an enterprise's expired licenses and, for each license, call the LMS to unlink the learner from the enterprise.
JIRA: https://2u-internal.atlassian.net/browse/ENT-9667

Testing considerations

  • Include instructions for any required manual tests, and any manual testing that has
    already been performed.
  • Include unit and a11y tests as appropriate
  • Consider performance issues.
  • Check that Database migrations are backwards-compatible

Post-review

Squash commits into discrete sets of changes

@muhammad-ammar muhammad-ammar marked this pull request as draft October 31, 2024 07:53
@muhammad-ammar muhammad-ammar changed the title feat: process expired licenses feat: process expired licenses and unlink enterprise learners Oct 31, 2024

queryset = License.objects.filter(
status__in=[ASSIGNED, ACTIVATED],
renewed_to=None,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to Reviewers: I am not sure if renewed_to=None should be set or not? I got this from

expired_licenses = [lcs for lcs in subscription_plan_obj.licenses.all() if not lcs.renewed_to]

expired_subscription_plans = list(
SubscriptionPlan.objects.filter(
customer_agreement=customer_agreement,
expiration_date__lt=now,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to Reviewers: In expire_subscriptions.py we are using expiration_date__lte. Shouldn't we be using expiration_date__lt?

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, good question. I think from a business perspective, the expiration date configured on subscription plans is exclusive of the active period for the plan. For example, an expiration date of 2025-01-01 means the plan is valid through 2024-12-31 at midnight.

@muhammad-ammar muhammad-ammar marked this pull request as ready for review October 31, 2024 12:36
expired_subscription_plans = list(
SubscriptionPlan.objects.filter(
customer_agreement=customer_agreement,
expiration_date__lt=now,
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, good question. I think from a business perspective, the expiration date configured on subscription plans is exclusive of the active period for the plan. For example, an expiration date of 2025-01-01 means the plan is valid through 2024-12-31 at midnight.

Comment on lines 106 to 112
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make this logic part of the existing expire_subscriptions management command? If we don't, we might be in a situation where we have two crons running around the same time, where expire_subscriptions tries to downgrade all the course enrollments for licensed learners to audit mode, and this new process_expired_licenses command tries to unlink learners from an enterprise. If the second one executes first, it might cause the first one to fail.
Inside the _expire_subscription_plan() method would be a good place for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • So you are saying, we should move the unlink logic into expire_subscriptions management command, correct?
  • Would it be ok if I trigger a new async task right after license_expiration_task? Any chance that unlinking happens before downgrading the enrollments? OR should I do unlinking without an async task?

Copy link
Contributor

Choose a reason for hiding this comment

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

The expire_subscriptions command currently calls the license_expiration_task() synchronously (i.e. without .apply_async()). I would also do the unlinking synchronously from the management command, since we're not in the context of a web request and don't really care if this management command takes several minutes to complete. I would generally only invoke async tasks from a management command if there's lots and lots of work to do and the work is inherently parallelizable (for example, when we re-sync data in enterprise-catalog).

Copy link
Contributor

Choose a reason for hiding this comment

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

And yes, correct on your first question.

@@ -477,3 +477,5 @@
]

CUSTOMERS_WITH_CUSTOM_LICENSE_EVENTS = ['00000000-1111-2222-3333-444444444444']
CUSTOMERS_WITH_EXPIRED_LICENSES_UNLINKING_ENABLED = ['76b933cb-bf2a-4c1e-bf44-4e8a58cc37ae']
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be an empty list in base settings.

@muhammad-ammar muhammad-ammar force-pushed the ammar/process-expired-licenses branch 3 times, most recently from e8396ff to 69aabec Compare November 4, 2024 06:41
Comment on lines +77 to +79
queryset = License.objects.filter(
status__in=[ASSIGNED, ACTIVATED],
renewed_to=None,
subscription_plan__uuid__in=expired_subscription_plan_uuids,
).select_related(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to Reviewers: Is the above filtering correct? Specifically asking regarding status and renewed_to.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes this is correct.

Comment on lines 66 to 75
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to Reviewers: Do we still need this logic? Now that we are only picking plans expired 90 days ago?

Copy link
Contributor

Choose a reason for hiding this comment

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

No you shouldn't need this anymore.

Comment on lines 66 to 75
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)
Copy link
Contributor

Choose a reason for hiding this comment

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

No you shouldn't need this anymore.

Comment on lines +77 to +79
queryset = License.objects.filter(
status__in=[ASSIGNED, ACTIVATED],
renewed_to=None,
subscription_plan__uuid__in=expired_subscription_plan_uuids,
).select_related(
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes this is correct.

Comment on lines 139 to 145
other_active_licenses = License.for_user_and_customer(
user_email=license.user_email,
lms_user_id=license.lms_user_id,
enterprise_customer_uuid=enterprise_customer_uuid,
active_plans_only=True,
current_plans_only=True,
).exists()
if other_active_licenses:
continue
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

Copy link
Contributor

@macdiesel macdiesel left a comment

Choose a reason for hiding this comment

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

This looks good. I am going to approve to unblock this work.

Before merging please ensure that is_relinkable is set to true when unlinking these learners.

enterprise_customer_uuid,
{
'user_emails': user_emails,
'is_relinkable': False
Copy link
Contributor

Choose a reason for hiding this comment

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

We had a change of requirements from Joe on this. Decision was made here: https://twou.slack.com/archives/C07UVDDRRUZ/p1730470547546569

is_relinkable should be set to true.

@muhammad-ammar muhammad-ammar force-pushed the ammar/process-expired-licenses branch 2 times, most recently from 0cee565 to 786a8fc Compare November 5, 2024 02:58
@muhammad-ammar
Copy link
Contributor Author

@iloveagent57 @macdiesel Thank you for the review. I have addressed your feedback and also added unit tests. I will do some more manual testing and then merge.

@muhammad-ammar muhammad-ammar force-pushed the ammar/process-expired-licenses branch 5 times, most recently from 5c67922 to b7238bf Compare November 5, 2024 07:34
@muhammad-ammar muhammad-ammar merged commit 2d55d76 into master Nov 6, 2024
5 checks passed
@muhammad-ammar muhammad-ammar deleted the ammar/process-expired-licenses branch November 6, 2024 05:24
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