From 0dd109381261ebdc74191c7539df013786bced54 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sun, 9 Aug 2020 11:02:45 +0200 Subject: [PATCH] add sample products and improve testing for Product --- .../migrations/0010_auto_20200809_0856.py | 65 +++++++++++++++++++ uncloud_pay/models.py | 41 ++++++++++-- uncloud_pay/tests.py | 46 ++++++++++++- uncloud_vm/models.py | 21 +++--- 4 files changed, 156 insertions(+), 17 deletions(-) create mode 100644 uncloud_pay/migrations/0010_auto_20200809_0856.py diff --git a/uncloud_pay/migrations/0010_auto_20200809_0856.py b/uncloud_pay/migrations/0010_auto_20200809_0856.py new file mode 100644 index 0000000..db2f7d7 --- /dev/null +++ b/uncloud_pay/migrations/0010_auto_20200809_0856.py @@ -0,0 +1,65 @@ +# Generated by Django 3.1 on 2020-08-09 08:56 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0009_auto_20200808_2113'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='depends_on', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_of', to='uncloud_pay.order'), + ), + migrations.AlterField( + model_name='order', + name='replaces', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replaced_by', to='uncloud_pay.order'), + ), + migrations.CreateModel( + name='SampleRecurringProductOneTimeFee', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extra_data', models.JSONField(blank=True, editable=False, null=True)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32)), + ('order', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.order')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SampleRecurringProduct', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extra_data', models.JSONField(blank=True, editable=False, null=True)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32)), + ('order', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.order')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='SampleOneTimeProduct', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('extra_data', models.JSONField(blank=True, editable=False, null=True)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32)), + ('order', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.order')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 7232cd3..8023bf7 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -596,7 +596,6 @@ class BillRecord(models.Model): return record_delta.total_seconds()/self.order.recurring_period - @property def sum(self): return self.order.price * Decimal(self.quantity) @@ -604,6 +603,12 @@ class BillRecord(models.Model): def __str__(self): return f"{self.bill}: {self.quantity} x {self.order}" + def save(self, *args, **kwargs): + if self.ending_date < self.starting_date: + raise ValidationError("End date cannot be before starting date") + + super().save(*args, **kwargs) + ### # Products @@ -703,13 +708,12 @@ class Product(UncloudModel): @property def recurring_price(self): - pass # To be implemented in child. + """ implement correct values in the child class """ + return 0 @property def one_time_price(self): - """ - Default is 0 CHF - """ + """ implement correct values in the child class """ return 0 @@ -799,3 +803,30 @@ class Product(UncloudModel): else: # FIXME: use the right type of exception here! raise Exception("Did not implement the discounter for this case") + +# Sample products included into uncloud +class SampleOneTimeProduct(Product): + + default_recurring_period = RecurringPeriod.ONE_TIME + + @property + def one_time_price(self): + return 5 + +class SampleRecurringProduct(Product): + default_recurring_period = RecurringPeriod.PER_30D + + @property + def recurring_price(self): + return 10 + +class SampleRecurringProductOneTimeFee(Product): + default_recurring_period = RecurringPeriod.PER_30D + + @property + def one_time_price(self): + return 5 + + @property + def recurring_price(self): + return 1 diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index 7acd7d4..21ac730 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -5,6 +5,7 @@ from datetime import datetime, date, timedelta from .models import * from uncloud_service.models import GenericServiceProduct + class ProductOrderTestCase(TestCase): """ Test products and products <-> order interaction @@ -15,12 +16,53 @@ class ProductOrderTestCase(TestCase): username='random_user', email='jane.random@domain.tld') - def test_update_one_time_product(self): + ba = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="somewhere else", + active=True) + + def test_create_one_time_product(self): """ One time payment products cannot be updated - can they? """ - pass + p = SampleOneTimeProduct.objects.create(owner=self.user) + + self.assertEqual(p.one_time_price, 5) + self.assertEqual(p.recurring_price, 0) + + + def test_create_order_creates_correct_order_count(self): + """ + Ensure creating orders from product only creates 1 order + """ + + # One order + p = SampleOneTimeProduct(owner=self.user) + p.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) + p.save() + + order_count = Order.objects.filter(owner=self.user).count() + self.assertEqual(order_count, 1) + + # One more order + p = SampleRecurringProduct(owner=self.user) + p.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) + p.save() + + order_count = Order.objects.filter(owner=self.user).count() + self.assertEqual(order_count, 2) + + # Should create 2 orders + p = SampleRecurringProductOneTimeFee(owner=self.user) + p.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) + p.save() + + order_count = Order.objects.filter(owner=self.user).count() + self.assertEqual(order_count, 4) class BillingAddressTestCase(TestCase): diff --git a/uncloud_vm/models.py b/uncloud_vm/models.py index 4033ba0..dc2369e 100644 --- a/uncloud_vm/models.py +++ b/uncloud_vm/models.py @@ -69,16 +69,6 @@ class VMProduct(Product): def recurring_price(self): return self.cores * 3 + self.ram_in_gb * 4 - def __str__(self): - if self.name: - name = f"{self.name} ({self.id})" - else: - name = self.id - - return "VM {}: {} cores {} gb ram".format(name, - self.cores, - self.ram_in_gb) - @property def description(self): return "Virtual machine '{}': {} core(s), {}GB memory".format( @@ -92,6 +82,17 @@ class VMProduct(Product): RecurringPeriod.choices)) + def __str__(self): + name = f"{self.id}" + + if self.name: + name = f"{self.id} ({self.name})" + + return "VM {}: {} cores {} gb ram".format(name, + self.cores, + self.ram_in_gb) + + class VMWithOSProduct(VMProduct): primary_disk = models.ForeignKey('VMDiskProduct', on_delete=models.CASCADE, null=True)