diff --git a/license_manager/apps/subscriptions/migrations/0057_auto_20230915_0722.py b/license_manager/apps/subscriptions/migrations/0057_auto_20230915_0722.py new file mode 100644 index 00000000..b938f09e --- /dev/null +++ b/license_manager/apps/subscriptions/migrations/0057_auto_20230915_0722.py @@ -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'), + ), + ] diff --git a/license_manager/apps/subscriptions/migrations/0058_subscriptionlicensesource_subscriptionlicensesourcetype.py b/license_manager/apps/subscriptions/migrations/0058_subscriptionlicensesource_subscriptionlicensesourcetype.py new file mode 100644 index 00000000..14fa5127 --- /dev/null +++ b/license_manager/apps/subscriptions/migrations/0058_subscriptionlicensesource_subscriptionlicensesourcetype.py @@ -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, + }, + ), + ] diff --git a/license_manager/apps/subscriptions/migrations/0059_add_subscriptionlicensesourcetypes.py b/license_manager/apps/subscriptions/migrations/0059_add_subscriptionlicensesourcetypes.py new file mode 100644 index 00000000..a0739ce2 --- /dev/null +++ b/license_manager/apps/subscriptions/migrations/0059_add_subscriptionlicensesourcetypes.py @@ -0,0 +1,34 @@ +# Generated by Django 3.2.21 on 2023-09-18 04:32 + +from django.db import migrations + + +LICENSE_SOURCE_TYPES = { + 'AMT': '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 + ) + + ] diff --git a/license_manager/apps/subscriptions/models.py b/license_manager/apps/subscriptions/models.py index 8b4302f6..39d10ef0 100644 --- a/license_manager/apps/subscriptions/models.py +++ b/license_manager/apps/subscriptions/models.py @@ -1291,6 +1291,67 @@ 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) + + 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. diff --git a/license_manager/apps/subscriptions/tests/factories.py b/license_manager/apps/subscriptions/tests/factories.py index d64dc0b8..7517fbaa 100644 --- a/license_manager/apps/subscriptions/tests/factories.py +++ b/license_manager/apps/subscriptions/tests/factories.py @@ -16,6 +16,8 @@ License, PlanType, Product, + SubscriptionLicenseSource, + SubscriptionLicenseSourceType, SubscriptionPlan, SubscriptionPlanRenewal, ) @@ -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 diff --git a/license_manager/apps/subscriptions/tests/test_models.py b/license_manager/apps/subscriptions/tests/test_models.py index 05949474..e8e7aa92 100644 --- a/license_manager/apps/subscriptions/tests/test_models.py +++ b/license_manager/apps/subscriptions/tests/test_models.py @@ -16,10 +16,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, ) @@ -386,3 +391,46 @@ 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 = 'bob@example.com' + 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', + )