Skip to content

Commit

Permalink
Merge pull request #523 from openedx/ammar/associate-opportunity-id-w…
Browse files Browse the repository at this point in the history
…ith-licenses

feat: add models to associate an opportunity id with a subscription license
  • Loading branch information
muhammad-ammar authored Sep 21, 2023
2 parents 0ab3c47 + f9367ed commit 9e8d68b
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .annotation_safe_list.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ waffle.Sample:
".. no_pii:": "This model has no PII"
waffle.Switch:
".. no_pii:": "This model has no PII"
subscriptions.HistoricalSubscriptionLicenseSource:
".. no_pii:": "This model has no PII"
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 3.2.21 on 2023-09-15 07:22

import django.core.validators
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('subscriptions', '0056_auto_20230530_1901'),
]

operations = [
migrations.AlterField(
model_name='historicalsubscriptionplan',
name='salesforce_opportunity_id',
field=models.CharField(blank=True, help_text='Deprecated -- 18 character value, derived from Salesforce Opportunity record.', max_length=18, null=True, validators=[django.core.validators.MinLengthValidator(18)]),
),
migrations.AlterField(
model_name='historicalsubscriptionplan',
name='salesforce_opportunity_line_item',
field=models.CharField(blank=True, help_text='18 character value -- Locate the appropriate Salesforce Opportunity Line Item record and copy it here.', max_length=18, null=True, validators=[django.core.validators.MinLengthValidator(18)]),
),
migrations.AlterField(
model_name='historicalsubscriptionplanrenewal',
name='salesforce_opportunity_id',
field=models.CharField(help_text='Locate the appropriate Salesforce Opportunity record and copy the Opportunity ID field (18 characters). Note that this is not the same Salesforce Opportunity ID associated with the linked subscription.', max_length=18, validators=[django.core.validators.MinLengthValidator(18)], verbose_name='Salesforce Opportunity Line Item'),
),
migrations.AlterField(
model_name='subscriptionplan',
name='salesforce_opportunity_id',
field=models.CharField(blank=True, help_text='Deprecated -- 18 character value, derived from Salesforce Opportunity record.', max_length=18, null=True, validators=[django.core.validators.MinLengthValidator(18)]),
),
migrations.AlterField(
model_name='subscriptionplan',
name='salesforce_opportunity_line_item',
field=models.CharField(blank=True, help_text='18 character value -- Locate the appropriate Salesforce Opportunity Line Item record and copy it here.', max_length=18, null=True, validators=[django.core.validators.MinLengthValidator(18)]),
),
migrations.AlterField(
model_name='subscriptionplanrenewal',
name='salesforce_opportunity_id',
field=models.CharField(help_text='Locate the appropriate Salesforce Opportunity record and copy the Opportunity ID field (18 characters). Note that this is not the same Salesforce Opportunity ID associated with the linked subscription.', max_length=18, validators=[django.core.validators.MinLengthValidator(18)], verbose_name='Salesforce Opportunity Line Item'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Generated by Django 3.2.21 on 2023-09-18 04:32

import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields


class Migration(migrations.Migration):

dependencies = [
('subscriptions', '0057_auto_20230915_0722'),
]

operations = [
migrations.CreateModel(
name='SubscriptionLicenseSourceType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('name', models.CharField(max_length=64)),
('slug', models.SlugField(max_length=30, unique=True)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SubscriptionLicenseSource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('source_id', models.CharField(help_text='18 character value -- Salesforce Opportunity ID', max_length=18, validators=[django.core.validators.MinLengthValidator(18)])),
('license', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='source', to='subscriptions.license')),
('source_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='subscriptions.subscriptionlicensesourcetype')),
],
options={
'abstract': False,
},
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.2.21 on 2023-09-18 04:32

from django.db import migrations


LICENSE_SOURCE_TYPES = {
'Application Management Technology': 'AMT'
}


def add_license_source_types(apps, schema_editor):
license_source_type_model = apps.get_model('subscriptions', 'SubscriptionLicenseSourceType')
for name, slug in LICENSE_SOURCE_TYPES.items():
license_source_type_model.objects.update_or_create(name=name, slug=slug)


def delete_license_source_types(apps, schema_editor):
license_source_type_model = apps.get_model('subscriptions', 'SubscriptionLicenseSourceType')
license_source_type_model.objects.filter(name__in=LICENSE_SOURCE_TYPES).delete()


class Migration(migrations.Migration):

dependencies = [
('subscriptions', '0058_subscriptionlicensesource_subscriptionlicensesourcetype'),
]

operations = [
migrations.RunPython(
code=add_license_source_types,
reverse_code=delete_license_source_types
)

]
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Generated by Django 3.2.21 on 2023-09-19 06:38

from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import simple_history.models


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('subscriptions', '0059_add_subscriptionlicensesourcetypes'),
]

operations = [
migrations.CreateModel(
name='HistoricalSubscriptionLicenseSource',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('source_id', models.CharField(help_text='18 character value -- Salesforce Opportunity ID', max_length=18, validators=[django.core.validators.MinLengthValidator(18)])),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField()),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('license', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='subscriptions.license')),
('source_type', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='subscriptions.subscriptionlicensesourcetype')),
],
options={
'verbose_name': 'historical subscription license source',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': 'history_date',
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]
68 changes: 68 additions & 0 deletions license_manager/apps/subscriptions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1291,6 +1291,74 @@ def _clean_up_duplicate_licenses(cls, duplicate_licenses):
return sorted_licenses[0]


class SubscriptionLicenseSourceType(TimeStampedModel):
"""
Subscription License Source Type
.. no_pii: This model has no PII
"""

AMT = 'AMT'

name = models.CharField(max_length=64)
slug = models.SlugField(max_length=30, unique=True)

@classmethod
def get_source_type(cls, source_slug):
"""
Retrieve the source type based on the slug.
"""
try:
return cls.objects.get(slug=source_slug)
except SubscriptionLicenseSourceType.DoesNotExist:
return None

def __str__(self):
"""
String representation of source type.
"""
return "SubscriptionLicenseSourceType: Name: {name}, Slug: {slug}".format(name=self.name, slug=self.slug)


class SubscriptionLicenseSource(TimeStampedModel):
"""
Subscription License Source
.. no_pii: This model has no PII
"""

license = models.OneToOneField(
License,
related_name='source',
on_delete=models.CASCADE,
)
source_id = models.CharField(
max_length=SALESFORCE_ID_LENGTH,
validators=[MinLengthValidator(SALESFORCE_ID_LENGTH)],
help_text=_("18 character value -- Salesforce Opportunity ID")
)
source_type = models.ForeignKey(SubscriptionLicenseSourceType, on_delete=models.CASCADE)

history = HistoricalRecords()

def save(self, *args, **kwargs):
"""
Override to ensure that model.clean is always called.
"""
self.full_clean()
return super().save(*args, **kwargs)

def __str__(self):
"""
String representation of source.
"""
return "SubscriptionLicenseSource: LicenseID: {license}, SourceID: {source}, SourceType: {source_type}".format(
license=self.license.uuid,
source=self.source_id,
source_type=self.source_type.slug,
)


class SubscriptionsFeatureRole(UserRole):
"""
User role definitions specific to subscriptions.
Expand Down
15 changes: 15 additions & 0 deletions license_manager/apps/subscriptions/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
License,
PlanType,
Product,
SubscriptionLicenseSource,
SubscriptionLicenseSourceType,
SubscriptionPlan,
SubscriptionPlanRenewal,
)
Expand Down Expand Up @@ -160,3 +162,16 @@ class UserFactory(factory.django.DjangoModelFactory):

class Meta:
model = User


class SubscriptionLicenseSourceFactory(factory.django.DjangoModelFactory):
"""
Test factory for the `SubscriptionLicenseSource` model.
"""

license = factory.SubFactory(LicenseFactory)
source_id = factory.LazyFunction(get_random_salesforce_id)
source_type = factory.Iterator(SubscriptionLicenseSourceType.objects.all())

class Meta:
model = SubscriptionLicenseSource
65 changes: 64 additions & 1 deletion license_manager/apps/subscriptions/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import ddt
import freezegun
import pytest
from django.forms import ValidationError
from django.test import TestCase
from requests.exceptions import HTTPError
Expand All @@ -16,10 +17,15 @@
SegmentEvents,
)
from license_manager.apps.subscriptions.exceptions import CustomerAgreementError
from license_manager.apps.subscriptions.models import License, Notification
from license_manager.apps.subscriptions.models import (
License,
Notification,
SubscriptionLicenseSourceType,
)
from license_manager.apps.subscriptions.tests.factories import (
CustomerAgreementFactory,
LicenseFactory,
SubscriptionLicenseSourceFactory,
SubscriptionPlanFactory,
SubscriptionPlanRenewalFactory,
)
Expand Down Expand Up @@ -386,3 +392,60 @@ def test_net_days_until_expiration(self):
with freezegun.freeze_time(today):
expected_days = (self.subscription_plan_b.expiration_date - today).days
assert self.customer_agreement.net_days_until_expiration == expected_days


@ddt.ddt
class SubscriptionLicenseSourceModelTests(TestCase):
"""
Tests for the `SubscriptionLicenseSource` model.
"""

def setUp(self):
super().setUp()

self.user_email = '[email protected]'
self.enterprise_customer_uuid = uuid.uuid4()
self.customer_agreement = CustomerAgreementFactory.create(
enterprise_customer_uuid=self.enterprise_customer_uuid,
)

self.active_current_plan = SubscriptionPlanFactory.create(
customer_agreement=self.customer_agreement,
is_active=True,
start_date=localized_datetime(2021, 1, 1),
expiration_date=localized_datetime_from_datetime(datetime.now() + timedelta(days=365)),
)

self.active_current_license = LicenseFactory.create(
user_email=self.user_email,
subscription_plan=self.active_current_plan,
)

def test_license_source_creation(self):
"""
Tests license souce model object creation.
"""
license_source = SubscriptionLicenseSourceFactory(
license=self.active_current_license,
source_id='000000000000000000',
source_type=SubscriptionLicenseSourceType.get_source_type(SubscriptionLicenseSourceType.AMT)
)
str_repr = 'SubscriptionLicenseSource: LicenseID: {license_uuid}, SourceID: {source_id}, SourceType: AMT'
assert str(license_source) == str_repr.format(
license_uuid=self.active_current_license.uuid,
source_id='000000000000000000',
)

def test_license_source_creation_with_invalid_souce_id(self):
"""
Verify that SubscriptionLicenseSource model raises exception if source id format is wrong.
"""
with pytest.raises(ValidationError) as raised_exception:
SubscriptionLicenseSourceFactory(
license=self.active_current_license,
source_id='000000000',
source_type=SubscriptionLicenseSourceType.get_source_type(SubscriptionLicenseSourceType.AMT)
)

exception_message = ['Ensure this value has at least 18 characters (it has 9).']
assert raised_exception.value.args[0]['source_id'][0].messages == exception_message

0 comments on commit 9e8d68b

Please sign in to comment.