Exemple #1
0
class SubscriptionPlanFactory(factory.django.DjangoModelFactory):
    """
    Test factory for the `SubscriptionPlan` model.

    Creates a subscription purchased and starting today by default.
    """
    class Meta:
        model = SubscriptionPlan

    # Make the title sufficiently random to avoid violating
    # the unique constraint on (customer_agreement, title)
    title = factory.LazyAttribute(
        lambda p: '{} {}'.format(FAKER.word(), random.randint(0, 1000000)))
    uuid = factory.LazyFunction(uuid4)
    is_active = True
    start_date = localized_utcnow()
    # Make the subscription expire in roughly a year and a day
    expiration_date = localized_utcnow() + timedelta(days=366)
    expiration_processed = False
    customer_agreement = factory.SubFactory(CustomerAgreementFactory)
    enterprise_catalog_uuid = factory.LazyFunction(uuid4)
    salesforce_opportunity_id = factory.LazyFunction(get_random_salesforce_id)
    product = factory.SubFactory(ProductFactory)
    can_freeze_unused_licenses = False
    should_auto_apply_licenses = False
Exemple #2
0
def make_bound_subscription_form(
        title=faker.pystr(min_chars=1, max_chars=127),
        start_date=localized_utcnow(),
        expiration_date=localized_utcnow() + timedelta(days=366),
        enterprise_catalog_uuid=faker.uuid4(),
        enterprise_customer_uuid=faker.uuid4(),
        salesforce_opportunity_id=get_random_salesforce_id(),
        num_licenses=0,
        is_active=False,
        for_internal_use_only=False,
        has_product=True,
        is_sf_id_required=False,
        has_customer_agreement=True,
        customer_agreement_has_default_catalog=True,
        change_reason="new"):
    """
    Builds a bound SubscriptionPlanForm
    """
    if customer_agreement_has_default_catalog:
        customer_agreement = CustomerAgreementFactory(
            enterprise_customer_uuid=enterprise_customer_uuid)
    else:
        customer_agreement = CustomerAgreementFactory(
            enterprise_customer_uuid=enterprise_customer_uuid,
            default_enterprise_catalog_uuid=None)

    product = ProductFactory(plan_type=PlanTypeFactory(
        sf_id_required=is_sf_id_required))

    form_data = {
        'title':
        title,
        'start_date':
        start_date,
        'expiration_date':
        expiration_date,
        'enterprise_catalog_uuid':
        enterprise_catalog_uuid,
        'product':
        product.id if has_product else None,
        'salesforce_opportunity_id':
        salesforce_opportunity_id,
        'num_licenses':
        num_licenses,
        'is_active':
        is_active,
        'for_internal_use_only':
        for_internal_use_only,
        'customer_agreement':
        str(customer_agreement.uuid) if has_customer_agreement else None,
        'change_reason':
        change_reason
    }
    return SubscriptionPlanForm(form_data)
Exemple #3
0
def get_context_from_subscription_plan_by_activation_key(request):
    """
    Helper function to return the permission context (i.e., enterprise customer uuid) from active
    subscription plan associated with the license identified by the ``activation_key`` query param
    on a request and the ``email`` provided in the request's JWT.

    Params:
        ``request`` - A DRF Request object.

    Returns: The ``enterprise_customer_uuid`` associated with the user's license.
    """
    today = localized_utcnow()
    activation_key = get_activation_key_from_request(request)

    try:
        user_license = License.objects.get(
            activation_key=activation_key,
            user_email=get_email_from_request(request),
            subscription_plan__is_active=True,
            subscription_plan__start_date__lte=today,
            subscription_plan__expiration_date__gte=today,
        )
    except License.DoesNotExist as exc:
        decoded_jwt = get_decoded_jwt(request)
        lms_user_id = get_key_from_jwt(decoded_jwt, 'user_id')
        logger.exception(
            'License not found for activation key %s for user %s',
            activation_key,
            lms_user_id
        )
        raise Http404('No License matches the given query.') from exc

    return user_license.subscription_plan.customer_agreement.enterprise_customer_uuid
Exemple #4
0
    def is_valid(self):
        # Perform original validation and return if false
        if not super().is_valid():
            return False

        # Subscription dates should follow this ordering:
        # subscription start date <= subscription expiration date <= subscription renewal effective date <=
        # subscription renewal expiration date
        form_effective_date = self.cleaned_data.get('effective_date')
        form_renewed_expiration_date = self.cleaned_data.get(
            'renewed_expiration_date')

        if form_effective_date < localized_utcnow():
            self.add_error(
                'effective_date',
                'A subscription renewal can not be scheduled to become effective in the past.',
            )
            return False

        if form_renewed_expiration_date < form_effective_date:
            self.add_error(
                'renewed_expiration_date',
                'A subscription renewal can not expire before it becomes effective.',
            )
            return False

        subscription = self.instance.prior_subscription_plan
        if form_effective_date < subscription.expiration_date:
            self.add_error(
                'effective_date',
                'A subscription renewal can not take effect before a subscription expires.',
            )
            return False

        return True
Exemple #5
0
    def for_user_and_customer(
        cls,
        user_email,
        lms_user_id,
        enterprise_customer_uuid,
        active_plans_only=False,
        current_plans_only=False,
    ):
        """
        Returns all licenses asssociated with the given user email or lms_user_id that are associated
        with a particular customer's SubscrptionPlan. The optional ``active_plans_only``
        and ``current_plans_only`` allow the caller to filter for licenses whose plans
        are marked ``active`` or that are current (the current time is within the plan's
        start/end range), respectively.
        """
        queryset = cls.by_user_email_or_lms_user_id(user_email, lms_user_id)
        kwargs = {
            'subscription_plan__customer_agreement__enterprise_customer_uuid': enterprise_customer_uuid,
        }
        if active_plans_only:
            kwargs['subscription_plan__is_active'] = True
        if current_plans_only:
            now = localized_utcnow()
            kwargs['subscription_plan__start_date__lte'] = now
            kwargs['subscription_plan__expiration_date__gte'] = now

        return queryset.filter(**kwargs)
Exemple #6
0
    def get_licenses_exceeding_purge_duration(cls, date_field_to_compare, **kwargs):
        """
        Returns all licenses with non-null ``user_email`` values
        that have exceeded the purge duration specified by the related
        plan's ``CustomerAgreement.license_duration_before_purge`` value.

        The ``date_field_to_compare`` argument is compared to this value to determine
        if the duration has been exceeded.  It can be the name of any valid
        field for a queryset that filters on ``License`` or a related
        ``SubscriptionPlan`` or ``CustomerAgreement`` - for example:

          'activation_date'
          'revoked_date'
          'subscription_plan__expiration_date'
          'subscription_plan__start_date'
        """
        duration_before_purge_field = 'subscription_plan__customer_agreement__license_duration_before_purge'
        date_field = date_field_to_compare + '__lt'
        date_field_is_null = date_field_to_compare + '__isnull'

        kwargs.update({
            'user_email__isnull': False,
            date_field_is_null: False,
            date_field: localized_utcnow() - models.F(duration_before_purge_field),
        })

        return License.objects.filter(**kwargs).select_related(
            'subscription_plan',
            'subscription_plan__customer_agreement',
        )
 def revoke(self):
     """
     Performs all field updates required to revoke a License
     """
     self.status = REVOKED
     self.revoked_date = localized_utcnow()
     self.save()
    def handle(self, *args, **options):
        now = localized_utcnow()
        renewal_processing_window_cutoff = now + timedelta(hours=int(options['processing_window_length_hours']))

        renewals_to_be_processed = SubscriptionPlanRenewal.objects.filter(
            effective_date__gte=now, effective_date__lte=renewal_processing_window_cutoff, processed=False
        ).select_related(
            'prior_subscription_plan',
            'prior_subscription_plan__customer_agreement',
            'renewed_subscription_plan'
        )

        subscription_uuids = [str(renewal.prior_subscription_plan.uuid) for renewal in renewals_to_be_processed]

        if not options['dry_run']:
            logger.info('Processing {} renewals for subscriptions with uuids: {}'.format(
                len(subscription_uuids), subscription_uuids))

            renewed_subscription_uuids = []
            for renewal in renewals_to_be_processed:
                subscription_uuid = str(renewal.prior_subscription_plan.uuid)
                try:
                    renew_subscription(renewal, is_auto_renewed=True)
                    renewed_subscription_uuids.append(subscription_uuid)
                except RenewalProcessingError:
                    logger.error('Could not automatically process renewal with id: {}'.format(
                        renewal.id), exc_info=True)

            logger.info('Processed {} renewals for subscriptions with uuids: {}'.format(
                        len(renewed_subscription_uuids), renewed_subscription_uuids))
        else:
            logger.info('Dry-run result subscriptions that would be renewed: {} '.format(
                        subscription_uuids))
Exemple #9
0
 def test_is_locked_for_renewal_processing(self, is_locked_for_renewal_processing):
     today = localized_utcnow()
     with freezegun.freeze_time(today):
         renewed_subscription_plan = SubscriptionPlanFactory.create(expiration_date=today)
         renewal_kwargs = {'prior_subscription_plan': renewed_subscription_plan}
         if is_locked_for_renewal_processing:
             renewal_kwargs.update({'effective_date': renewed_subscription_plan.expiration_date})
         SubscriptionPlanRenewalFactory.create(**renewal_kwargs)
         self.assertEqual(renewed_subscription_plan.is_locked_for_renewal_processing, is_locked_for_renewal_processing)
Exemple #10
0
 def test_invalid_start_date_before_today(self):
     prior_subscription_plan = SubscriptionPlanFactory.create()
     form = make_bound_subscription_plan_renewal_form(
         prior_subscription_plan=prior_subscription_plan,
         effective_date=localized_utcnow() - timedelta(1),
         renewed_expiration_date=prior_subscription_plan.expiration_date +
         timedelta(366),
     )
     assert not form.is_valid()
    def post(self, request):
        """
        Activates a license, given an ``activation_key`` query param (which should be a UUID).

        Route: /api/v1/license-activation?activation_key=your-key

        Returns:
            * 400 Bad Request - if the ``activation_key`` query parameter is malformed or missing, or if
                the user's email could not be found in the jwt.
            * 401 Unauthorized - if the requesting user is not authenticated.
            * 403 Forbidden - if the requesting user is not allowed to access the associated
                 license's subscription plan.
            * 404 Not Found - if the email found in the request's JWT and the provided ``activation_key``
                 do not match those of any existing license in an activate subscription plan.
            * 204 No Content - if such a license was found, and if the license is currently ``assigned``,
                 it is updated with a status of ``activated``, its ``activation_date`` is set, and its ``lms_user_id``
                 is updated to the value found in the request's JWT.  If the license is already ``activated``,
                 no update is made to it.
            * 422 Unprocessable Entity - if we find a license, but it's status is not currently ``assigned``
                 or ``activated``, we do nothing and return a 422 with a message indicating that the license
                 cannot be activated.
        """
        activation_key_uuid = utils.get_activation_key_from_request(request)
        try:
            user_license = License.objects.get(
                activation_key=activation_key_uuid,
                user_email=self.user_email,
                subscription_plan__is_active=True,
            )
        except License.DoesNotExist:
            msg = 'No license exists for the email {} with activation key {}'.format(
                self.user_email,
                activation_key_uuid,
            )
            return Response(msg, status=status.HTTP_404_NOT_FOUND)

        if user_license.status not in (constants.ASSIGNED,
                                       constants.ACTIVATED):
            return Response(
                f'Cannot activate a license with a status of {user_license.status}',
                status=status.HTTP_422_UNPROCESSABLE_ENTITY,
            )

        if user_license.status == constants.ASSIGNED:
            user_license.status = constants.ACTIVATED
            user_license.activation_date = localized_utcnow()
            user_license.lms_user_id = self.lms_user_id
            user_license.save()

            # Following successful license activation, send learner an email
            # to help them get started using the Enterprise Learner Portal.
            send_onboarding_email_task.delay(
                user_license.subscription_plan.enterprise_customer_uuid,
                user_license.user_email,
            )

        return Response(status=status.HTTP_204_NO_CONTENT)
Exemple #12
0
    def test_retire_old_licenses(self, _):
        """
        Verify that the command retires the correct licenses appropriately and logs messages about the retirement.
        """
        with freeze_time(
                localized_utcnow()), self.assertLogs(level='INFO') as log:
            call_command(self.command_name)

            # Verify all expired licenses that were ready for retirement have been retired correctly
            expired_licenses = self.expired_subscription_plan.licenses.all()
            assert_date_fields_correct(expired_licenses, ['revoked_date'],
                                       True)
            for expired_license in expired_licenses:
                expired_license.refresh_from_db()
                assert_pii_cleared(expired_license)
                assert expired_license.status == REVOKED
                assert_historical_pii_cleared(expired_license)
            message = 'Retired {} expired licenses with uuids: {}'.format(
                expired_licenses.count(),
                sorted([
                    expired_license.uuid
                    for expired_license in expired_licenses
                ]),
            )
            assert message in log.output[0]

            # Verify all revoked licenses that were ready for retirement have been retired correctly
            for revoked_license in self.revoked_licenses_ready_for_retirement:
                revoked_license.refresh_from_db()
                assert_pii_cleared(revoked_license)
                assert_historical_pii_cleared(revoked_license)
            message = 'Retired {} revoked licenses with uuids: {}'.format(
                self.num_revoked_licenses_to_retire,
                sorted([
                    revoked_license.uuid for revoked_license in
                    self.revoked_licenses_ready_for_retirement
                ]),
            )
            assert message in log.output[1]

            # Verify all assigned licenses that were ready for retirement have been retired correctly
            for assigned_license in self.assigned_licenses_ready_for_retirement:
                assigned_license.refresh_from_db()
                assert_license_fields_cleared(assigned_license)
                assert_pii_cleared(assigned_license)
                assert_historical_pii_cleared(assigned_license)
                assert assigned_license.activation_key is None
                assert assigned_license.status == UNASSIGNED
            message = 'Retired {} assigned licenses that exceeded their inactivation duration with uuids: {}'.format(
                self.num_assigned_licenses_to_retire,
                sorted([
                    assigned_license.uuid for assigned_license in
                    self.assigned_licenses_ready_for_retirement
                ]),
            )
            assert message in log.output[2]
    def add_arguments(self, parser):
        parser.add_argument(
            '--expired-after',
            action='store',
            dest='expiration_date_from',
            help=
            'The oldest expiration date for subscriptions to be processed, can be used with --expired-before to '
            'set a date range formatted as %Y-%m-%dT%H:%M:%S',
            default=(localized_utcnow() -
                     timedelta(days=1)).strftime(DATE_FORMAT))

        parser.add_argument(
            '--expired-before',
            action='store',
            dest='expiration_date_to',
            help=
            'The most recent expiration date for subscriptions to be processed, can be used with --expired-after '
            'to set a date range formatted as %Y-%m-%dT%H:%M:%S',
            default=localized_utcnow().strftime(DATE_FORMAT))

        parser.add_argument(
            '--subscription-uuids',
            action='store',
            dest='subscription_uuids',
            help=
            'Delimited subscription uuids used to specify which subscription plans should be expired.',
            type=lambda s: [str(uuid) for uuid in s.split(',')])

        parser.add_argument(
            '--dry-run',
            action='store',
            dest='dry_run',
            help=
            'Used to see which subscriptions would be processed by running this command without making changes',
            default=False)

        parser.add_argument(
            '--force',
            action='store_true',
            dest='force',
            default=False,
        )
Exemple #14
0
 def revoke(self):
     """
     Performs all field updates required to revoke a License
     """
     self.status = REVOKED
     self.revoked_date = localized_utcnow()
     self.save()
     event_properties = get_license_tracking_properties(self)
     track_event(self.lms_user_id,
                 SegmentEvents.LICENSE_REVOKED,
                 event_properties)
Exemple #15
0
    def set_date_fields_to_now(licenses, date_field_names):
        """
        Helper function to bulk set the field given by `date_field_name` on a group of licenses to now.

        Args:
            licenses (iterable): The licenses to set the field to now on.
            date_field_name (list of str): The names of the date field to set to now.
        """
        for subscription_license in licenses:
            for field_name in date_field_names:
                setattr(subscription_license, field_name, localized_utcnow())
        License.bulk_update(licenses, date_field_names)
Exemple #16
0
    def auto_applicable_subscription(self):
        """
        Get which subscription on CustomerAgreement is auto-applicable.
        """
        now = localized_utcnow()
        plan = self.subscriptions.filter(
            should_auto_apply_licenses=True,
            is_active=True,
            start_date__lte=now,
            expiration_date__gte=now
        ).first()

        return plan
Exemple #17
0
def assert_date_fields_correct(licenses, date_field_names, should_be_updated):
    """
    Helper that verifies that all of the given licenses have had the given date fields updated if applicable.

    If they should not have been updated, then it checks that the fields given by `date_field_names` is still None.
    """
    for license_obj in licenses:
        license_obj.refresh_from_db()
        if should_be_updated:
            for field_name in date_field_names:
                assert getattr(license_obj, field_name) == localized_utcnow()
        else:
            for field_name in date_field_names:
                assert getattr(license_obj, field_name) is None
Exemple #18
0
    def test_auto_applied_licenses_count_since(self):
        """
        Tests that the correct auto-applied license count is returned.
        """
        subscription_plan = SubscriptionPlanFactory.create(should_auto_apply_licenses=True)
        timestamp_1 = localized_utcnow()
        LicenseFactory.create_batch(1, subscription_plan=subscription_plan, auto_applied=True, activation_date=timestamp_1)
        LicenseFactory.create_batch(3, subscription_plan=subscription_plan, auto_applied=False, activation_date=timestamp_1)

        self.assertEqual(subscription_plan.auto_applied_licenses_count_since(), 1)
        timestamp_2 = timestamp_1 + timedelta(seconds=1)
        self.assertEqual(subscription_plan.auto_applied_licenses_count_since(timestamp_2), 0)
        LicenseFactory.create_batch(5, subscription_plan=subscription_plan, auto_applied=True, activation_date=timestamp_2)
        self.assertEqual(subscription_plan.auto_applied_licenses_count_since(timestamp_2), 5)
Exemple #19
0
def test_track_event_via_braze_alias(mock_braze_client):
    test_email = '*****@*****.**'
    assigned_license = LicenseFactory.create(
        subscription_plan=SubscriptionPlanFactory.create(),
        lms_user_id=5,
        user_email=test_email,
        status=ASSIGNED,
        auto_applied=True)
    test_event_name = 'test-event-name'
    test_event_properties = {
        'license_uuid':
        str(assigned_license.uuid),
        'enterprise_customer_slug':
        assigned_license.subscription_plan.customer_agreement.
        enterprise_customer_slug,
    }
    expected_user_alias = {
        "alias_name": test_email,
        "alias_label": ENTERPRISE_BRAZE_ALIAS_LABEL,
    }
    expected_attributes = {
        "user_alias":
        expected_user_alias,
        "email":
        test_email,
        "is_enterprise_learner":
        True,
        "_update_existing_only":
        False,
        "license_uuid":
        str(assigned_license.uuid),
        "enterprise_customer_slug":
        assigned_license.subscription_plan.customer_agreement.
        enterprise_customer_slug,
    }
    expected_event = {
        "user_alias": expected_user_alias,
        "name": test_event_name,
        "time": _iso_8601_format_string(localized_utcnow()),
        "properties": test_event_properties,
        "_update_existing_only": False,
    }
    _track_event_via_braze_alias(test_email, test_event_name,
                                 test_event_properties)
    mock_braze_client().create_braze_alias.assert_any_call(
        [test_email], ENTERPRISE_BRAZE_ALIAS_LABEL)
    mock_braze_client().track_user.assert_any_call(
        attributes=[expected_attributes], events=[expected_event])
Exemple #20
0
def send_initial_utilization_email_task(subscription_uuid):
    """
    Sends email to admins detailing license utilization for a subscription plan after the initial week.

    Arguments:
        subscription_uuid (str): The subscription plan's uuid
    """

    subscription = SubscriptionPlan.objects.get(uuid=subscription_uuid)

    # check if the email has been sent for the subscription plan already
    # we only want to send this email once for any given plan
    has_email_been_sent_before = Notification.objects.filter(
        subscripton_plan_id=subscription.uuid,
        notification_type=NotificationChoices.PERIODIC_INFORMATIONAL).count(
        ) > 0

    if has_email_been_sent_before:
        return

    # get the date when the subscription was last turned on
    auto_apply_licenses_turned_on_at = subscription.auto_apply_licenses_turned_on_at
    now = localized_utcnow()
    is_email_ready = (now - auto_apply_licenses_turned_on_at
                      ).days >= DAYS_BEFORE_INITIAL_UTILIZATION_EMAIL_SENT
    if not is_email_ready:
        return

    admin_users = _get_admin_users_for_enterprise(
        subscription.customer_agreement.enterprise_customer_uuid)

    with transaction.atomic():
        for admin_user in admin_users:
            notification = Notification.objects.create(
                enterprise_customer_uuid=subscription.customer_agreement.
                enterprise_customer_uuid,
                enterprise_customer_user_uuid=admin_user['ecu_id'],
                subscripton_plan_id=subscription.uuid,
                notification_type=NotificationChoices.PERIODIC_INFORMATIONAL)
            notification.save()

        # will raise exception and roll back changes if error occurs
        _send_license_utilization_email(
            subscription=subscription,
            campaign_id=settings.INITIAL_LICENSE_UTILIZATION_CAMPAIGN,
            users=admin_users)
Exemple #21
0
class SubscriptionPlanRenewalFactory(factory.django.DjangoModelFactory):
    """
    Test factory for the `SubscriptionPlanRenewal` model.

    Creates a subscription plan renewal with randomized data
    """
    class Meta:
        model = SubscriptionPlanRenewal

    prior_subscription_plan = factory.SubFactory(SubscriptionPlanFactory)
    renewed_subscription_plan = None
    salesforce_opportunity_id = factory.LazyFunction(get_random_salesforce_id)
    number_of_licenses = 5
    effective_date = localized_utcnow() + timedelta(days=366)
    renewed_expiration_date = effective_date + timedelta(days=366)
    processed = False
    disable_auto_apply_licenses = False
Exemple #22
0
    def handle(self, *args, **options):
        now = localized_utcnow()

        subscriptions = SubscriptionPlan.objects.filter(
            should_auto_apply_licenses=True,
            is_active=True,
            start_date__lte=now,
            expiration_date__gte=now).select_related('customer_agreement')

        if not subscriptions:
            logger.info(
                'No subscriptions with auto-applied licenses found, skipping license-utilization emails.'
            )
            return

        for subscription in subscriptions:
            send_initial_utilization_email_task.delay(subscription.uuid)
Exemple #23
0
def send_revocation_cap_notification_email_task(subscription_uuid):
    """
    Sends revocation cap email notification to ECS asynchronously.

    Arguments:
        subscription_uuid (str): UUID (string representation) of the subscription that has reached its recovation cap.
    """
    subscription_plan = SubscriptionPlan.objects.get(uuid=subscription_uuid)
    enterprise_api_client = EnterpriseApiClient()
    enterprise_customer = enterprise_api_client.get_enterprise_customer_data(
        subscription_plan.enterprise_customer_uuid)
    enterprise_name = enterprise_customer.get('name')

    now = localized_utcnow()
    revocation_date = datetime.strftime(now, "%B %d, %Y, %I:%M%p %Z")

    braze_campaign_id = settings.BRAZE_REVOKE_CAP_EMAIL_CAMPAIGN
    braze_trigger_properties = {
        'SUBSCRIPTION_TITLE': subscription_plan.title,
        'NUM_REVOCATIONS_APPLIED': subscription_plan.num_revocations_applied,
        'ENTERPRISE_NAME': enterprise_name,
        'REVOKED_LIMIT_REACHED_DATE': revocation_date,
    }
    recipient = _aliased_recipient_object_from_email(
        settings.CUSTOMER_SUCCESS_EMAIL_ADDRESS)

    try:
        braze_client_instance = BrazeApiClient()
        braze_client_instance.create_braze_alias(
            [settings.CUSTOMER_SUCCESS_EMAIL_ADDRESS],
            ENTERPRISE_BRAZE_ALIAS_LABEL,
        )
        braze_client_instance.send_campaign_message(
            braze_campaign_id,
            recipients=[recipient],
            trigger_properties=braze_trigger_properties,
        )

    except BrazeClientError as exc:
        message = 'Revocation cap notification email sending received an exception.'
        logger.exception(message)
        raise exc
    def _expire_subscription_plan(self, expired_subscription_plan):
        """
        Expires a single subscription plan.
        """
        expired_licenses = []

        for lcs in expired_subscription_plan.licenses.iterator():
            if lcs.status in [ASSIGNED, ACTIVATED]:
                expired_licenses.append(lcs)

        any_failures = False

        # Terminate the licensed course enrollments
        for license_chunk in chunks(expired_licenses,
                                    LICENSE_EXPIRATION_BATCH_SIZE):
            try:
                license_chunk_uuids = [str(lcs.uuid) for lcs in license_chunk]

                # We might be running this command against a plan that expired further in the past to fix bad data. We don't
                # want to modify a course enrollment if it's been modified after the plan expiration because a user might have upgraded
                # the course.
                ignore_enrollments_modified_after = expired_subscription_plan.expiration_date.isoformat() \
                    if expired_subscription_plan.expiration_date < localized_utcnow() - timedelta(days=1) else None

                license_expiration_task(license_chunk_uuids,
                                        ignore_enrollments_modified_after=
                                        ignore_enrollments_modified_after)
            except Exception:  # pylint: disable=broad-except
                any_failures = True
                msg = 'Failed to terminate course enrollments for learners in subscription: {}'.format(
                    expired_subscription_plan.uuid)
                logger.exception(msg)

        if not any_failures:
            message = 'Terminated course enrollments for learners in subscription: {}'.format(
                expired_subscription_plan.uuid)
            logger.info(message)

            expired_subscription_plan.expiration_processed = True
            expired_subscription_plan.save(
                update_fields=['expiration_processed'])
 def create_subscription_plan(self, customer_agreement, num_licenses=1):
     """
     Creates a SubscriptionPlan for a customer.
     """
     timestamp = localized_utcnow()
     new_plan = SubscriptionPlan(
         title='Seed Generated Plan from {} {}'.format(customer_agreement, timestamp),
         customer_agreement=customer_agreement,
         enterprise_catalog_uuid=customer_agreement.default_enterprise_catalog_uuid,
         start_date=timestamp,
         expiration_date=timestamp + timedelta(days=365),
         is_active=True,
         for_internal_use_only=True,
         salesforce_opportunity_id=123456789123456789,
         product=Product.objects.get(name="B2B Paid")
     )
     with transaction.atomic():
         new_plan.save()
         new_plan.increase_num_licenses(
             num_licenses
         )
     return new_plan
Exemple #26
0
    def populate_subscription_for_auto_applied_licenses_choices(
            self, instance):
        """
        Populates the choice field used to choose which plan
        is used for auto-applied licenses in a Customer Agreement.
        """
        now = localized_utcnow()
        active_plans = SubscriptionPlan.objects.filter(
            customer_agreement=instance,
            is_active=True,
            start_date__lte=now,
            expiration_date__gte=now)

        current_plan = instance.auto_applicable_subscription

        empty_choice = ('', '------')
        choices = [empty_choice] + [(plan.uuid, plan.title)
                                    for plan in active_plans]
        choice_field = forms.ChoiceField(
            choices=choices,
            required=False,
            initial=empty_choice if not current_plan else
            (current_plan.uuid, current_plan.title))
        self.fields['subscription_for_auto_applied_licenses'] = choice_field
Exemple #27
0
    def setUpTestData(cls):
        super().setUpTestData()

        subscription_plan = SubscriptionPlanFactory()
        day_before_retirement_deadline = localized_utcnow() - timedelta(
            DAYS_TO_RETIRE + 1)
        cls.expired_subscription_plan = SubscriptionPlanFactory(
            expiration_date=day_before_retirement_deadline)

        # Set up a bunch of licenses that should not be retired
        LicenseFactory.create_batch(
            3,
            status=ACTIVATED,
            subscription_plan=subscription_plan,
            revoked_date=None,
        )
        LicenseFactory.create_batch(
            4,
            status=REVOKED,
            subscription_plan=subscription_plan,
            revoked_date=localized_utcnow(),
        )
        LicenseFactory.create_batch(
            5,
            status=REVOKED,
            subscription_plan=subscription_plan,
            revoked_date=localized_utcnow(),
        )
        LicenseFactory.create_batch(
            5,
            status=REVOKED,
            subscription_plan=subscription_plan,
            revoked_date=day_before_retirement_deadline,
            user_email=
            None,  # The user_email is None to represent already retired licenses
        )
        LicenseFactory.create_batch(
            2,
            status=ASSIGNED,
            subscription_plan=subscription_plan,
            assigned_date=localized_utcnow(),
            revoked_date=None,
        )

        # Set up licenses that should be retired as either there subscription plan has been expired for long enough
        # for retirement, or they were assigned/revoked a day before the current date to retire.
        cls.num_revoked_licenses_to_retire = 6
        cls.revoked_licenses_ready_for_retirement = LicenseFactory.create_batch(
            cls.num_revoked_licenses_to_retire,
            status=REVOKED,
            subscription_plan=subscription_plan,
            revoked_date=day_before_retirement_deadline,
        )
        for revoked_license in cls.revoked_licenses_ready_for_retirement:
            revoked_license.lms_user_id = faker.random_int()
            revoked_license.user_email = faker.email()
            revoked_license.save()

        cls.num_assigned_licenses_to_retire = 7
        cls.assigned_licenses_ready_for_retirement = LicenseFactory.create_batch(
            cls.num_assigned_licenses_to_retire,
            status=ASSIGNED,
            subscription_plan=subscription_plan,
            assigned_date=day_before_retirement_deadline,
        )
        for assigned_license in cls.assigned_licenses_ready_for_retirement:
            assigned_license.lms_user_id = faker.random_int()
            assigned_license.user_email = faker.email()
            assigned_license.save()

        # Create licenses of different statuses that should be retired from association with an old expired subscription
        LicenseFactory.create(
            status=ACTIVATED,
            subscription_plan=cls.expired_subscription_plan,
            lms_user_id=faker.random_int(),
            user_email=faker.email(),
        )
        LicenseFactory.create(
            status=ASSIGNED,
            subscription_plan=cls.expired_subscription_plan,
            lms_user_id=faker.random_int(),
            user_email=faker.email(),
        )
        LicenseFactory.create(
            status=REVOKED,
            lms_user_id=faker.random_int(),
            user_email=faker.email(),
            subscription_plan=cls.expired_subscription_plan,
        )
    def assign(self, request, subscription_uuid=None):
        """
        Given a list of emails, assigns a license to those user emails and sends an activation email.

        This endpoint allows assigning licenses to users who have previously had a license revoked, by removing their
        association to the revoked licenses and then assigning them to unassigned licenses.
        """
        # Validate the user_emails and text sent in the data
        self._validate_data(request.data)
        # Dedupe all lowercase emails before turning back into a list for indexing
        user_emails = list(
            {email.lower()
             for email in request.data.get('user_emails', [])})

        subscription_plan = self._get_subscription_plan()

        # Find any emails that have already been associated with a non-revoked license in the subscription
        # and remove from user_emails list
        already_associated_licenses = subscription_plan.licenses.filter(
            user_email__in=user_emails,
            status__in=[constants.ASSIGNED, constants.ACTIVATED],
        )
        if already_associated_licenses:
            already_associated_emails = list(
                already_associated_licenses.values_list('user_email',
                                                        flat=True))
            for email in already_associated_emails:
                user_emails.remove(email.lower())

        # Get the revoked licenses that are attempting to be assigned to
        revoked_licenses_for_assignment = subscription_plan.licenses.filter(
            status=constants.REVOKED,
            user_email__in=user_emails,
        )

        # Make sure there are enough licenses that we can assign to
        num_user_emails = len(user_emails)
        num_unassigned_licenses = subscription_plan.unassigned_licenses.count()
        # Since we flip the status of revoked licenses when admins attempt to re-assign that learner to a new
        # license, we check that there are enough unassigned licenses when combined with the revoked licenses that
        # will have their status change
        num_potential_unassigned_licenses = num_unassigned_licenses + revoked_licenses_for_assignment.count(
        )
        if num_user_emails > num_potential_unassigned_licenses:
            msg = (
                'There are not enough licenses that can be assigned to complete your request.'
                'You attempted to assign {} licenses, but there are only {} potentially available.'
            ).format(num_user_emails, num_potential_unassigned_licenses)
            return Response(msg, status=status.HTTP_400_BAD_REQUEST)

        # Flip all revoked licenses that were associated with emails that we are assigning to unassigned, and clear
        # all the old data on the license.
        for revoked_license in revoked_licenses_for_assignment:
            revoked_license.reset_to_unassigned()

        License.bulk_update(
            revoked_licenses_for_assignment,
            [
                'status',
                'user_email',
                'lms_user_id',
                'last_remind_date',
                'activation_date',
                'activation_key',
                'assigned_date',
                'revoked_date',
            ],
        )

        # Get a queryset of only the number of licenses we need to assign
        unassigned_licenses = subscription_plan.unassigned_licenses[:
                                                                    num_user_emails]
        for unassigned_license, email in zip(unassigned_licenses, user_emails):
            # Assign each email to a license and mark the license as assigned
            unassigned_license.user_email = email
            unassigned_license.status = constants.ASSIGNED
            activation_key = str(uuid4())
            unassigned_license.activation_key = activation_key
            unassigned_license.assigned_date = localized_utcnow()
            unassigned_license.last_remind_date = localized_utcnow()

        License.bulk_update(
            unassigned_licenses,
            [
                'user_email', 'status', 'activation_key', 'assigned_date',
                'last_remind_date'
            ],
        )

        # Create async chains of the pending learners and activation emails tasks with each batch of users
        # The task signatures are immutable, hence the `si()` - we don't want the result of the
        # link_learners_to_enterprise_task passed to the "child" activation_email_task.
        for pending_learner_batch in chunks(
                user_emails, constants.PENDING_ACCOUNT_CREATION_BATCH_SIZE):
            chain(
                link_learners_to_enterprise_task.si(
                    pending_learner_batch,
                    subscription_plan.enterprise_customer_uuid,
                ),
                activation_email_task.si(
                    self._get_custom_text(request.data),
                    pending_learner_batch,
                    subscription_uuid,
                )).apply_async()

        # Pass email assignment data back to frontend for display
        response_data = {
            'num_successful_assignments': len(user_emails),
            'num_already_associated': len(already_associated_licenses)
        }
        return Response(data=response_data, status=status.HTTP_200_OK)
class ProcessRenewalsCommandTests(TestCase):
    command_name = 'process_renewals'
    now = localized_utcnow()

    def tearDown(self):
        """
        Deletes all renewals, licenses, and subscription after each test method is run.
        """
        super().tearDown()
        License.objects.all().delete()
        SubscriptionPlan.objects.all().delete()
        SubscriptionPlanRenewal.objects.all().delete()

    def create_subscription_with_renewal(self,
                                         effective_date,
                                         processed=False):
        prior_subscription_plan = SubscriptionPlanFactory.create(
            start_date=self.now - timedelta(days=7),
            expiration_date=self.now,
        )

        renewed_subscription_plan = SubscriptionPlanFactory.create(
            start_date=self.now,
            expiration_date=self.now + timedelta(days=7),
        )

        SubscriptionPlanRenewalFactory.create(
            prior_subscription_plan=prior_subscription_plan,
            renewed_subscription_plan=renewed_subscription_plan,
            effective_date=effective_date,
            processed=processed)

        return (prior_subscription_plan)

    @mock.patch(
        'license_manager.apps.subscriptions.management.commands.process_renewals.renew_subscription'
    )
    def test_no_upcoming_renewals(self, mock_renew_subscription):
        """
        Verify that only unprocessed renewals within their processing window are processed
        """
        with self.assertLogs(level='INFO') as log, freezegun.freeze_time(
                self.now):
            # renewal far in the future
            self.create_subscription_with_renewal(self.now + timedelta(
                hours=settings.SUBSCRIPTION_PLAN_RENEWAL_LOCK_PERIOD_HOURS) +
                                                  timedelta(seconds=1))

            # renewal in the past
            self.create_subscription_with_renewal(self.now -
                                                  timedelta(seconds=1))

            # renewal that has already been processed
            self.create_subscription_with_renewal(self.now +
                                                  timedelta(seconds=1),
                                                  processed=True)

            call_command(self.command_name)
            assert mock_renew_subscription.call_count == 0
            assert 'Processing 0 renewals for subscriptions with uuids: []' in log.output[
                0]
            assert 'Processed 0 renewals for subscriptions with uuids: []' in log.output[
                1]

    @mock.patch(
        'license_manager.apps.subscriptions.management.commands.process_renewals.renew_subscription'
    )
    def test_upcoming_renewals(self, mock_renew_subscription):
        """
        Verify that unprocessed renewals within their processing window are processed
        """
        subscription_plan_1 = self.create_subscription_with_renewal(
            self.now + timedelta(
                hours=settings.SUBSCRIPTION_PLAN_RENEWAL_LOCK_PERIOD_HOURS))

        subscription_plan_2 = self.create_subscription_with_renewal(
            self.now + timedelta(seconds=1))

        with self.assertLogs(level='INFO') as log, freezegun.freeze_time(
                self.now):
            call_command(self.command_name)
            assert mock_renew_subscription.call_count == 2
            assert "Processing 2 renewals for subscriptions with uuids: ['{}', '{}']".format(
                subscription_plan_1.uuid,
                subscription_plan_2.uuid) in log.output[0]
            assert "Processed 2 renewals for subscriptions with uuids: ['{}', '{}']".format(
                subscription_plan_1.uuid,
                subscription_plan_2.uuid) in log.output[1]

    @mock.patch(
        'license_manager.apps.subscriptions.management.commands.process_renewals.renew_subscription'
    )
    def test_renew_subscription_exception(self, mock_renew_subscription):
        """
        Verify that an exception when processing a renewal will not stop other renewals from being processed
        """
        mock_renew_subscription.side_effect = [RenewalProcessingError, None]

        subscription_plan_1 = self.create_subscription_with_renewal(
            self.now + timedelta(
                hours=settings.SUBSCRIPTION_PLAN_RENEWAL_LOCK_PERIOD_HOURS))

        subscription_plan_2 = self.create_subscription_with_renewal(
            self.now + timedelta(seconds=1))

        with self.assertLogs(level='INFO') as log, freezegun.freeze_time(
                self.now):
            call_command(self.command_name)
            assert mock_renew_subscription.call_count == 2
            assert "Processing 2 renewals for subscriptions with uuids: ['{}', '{}']".format(
                subscription_plan_1.uuid,
                subscription_plan_2.uuid) in log.output[0]
            assert "Could not automatically process renewal with id: {}".format(
                subscription_plan_1.renewal.id) in log.output[1]
            assert "Processed 1 renewals for subscriptions with uuids: ['{}']".format(
                subscription_plan_2.uuid) in log.output[2]
from license_manager.apps.api import tasks
from license_manager.apps.api.v1.tests.test_views import (
    LicenseViewSetActionMixin,
    LicenseViewTestMixin,
)
from license_manager.apps.subscriptions import api, constants, utils
from license_manager.apps.subscriptions.tests.factories import (
    LicenseFactory,
    SubscriptionPlanFactory,
    SubscriptionPlanRenewalFactory,
)

logger = logging.getLogger(__name__)

NOW = utils.localized_utcnow()


class EventTestCaseBase(TestCase):
    """
    Mocks the call to ``track_license_changes_task.delay()``
    to just be a simple python invocation of ``track_license_changes()``.
    """
    def setUp(self):
        super().setUp()
        self.mock_track_license_changes_mocker = mock.patch(
            'license_manager.apps.api.v1.views.track_license_changes_task.delay',
            wraps=tasks.track_license_changes_task,
        )
        self.mock_track_license_changes_mocker.start()