From 5d5bf486b584daed8b10e2d089cd164c8a65fc36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Tue, 10 Mar 2020 09:14:52 +0100 Subject: [PATCH 1/7] Initial product activation implementation --- .../commands/charge-negative-balance.py | 4 +- .../uncloud/uncloud_pay/models.py | 61 ++++++++++++------- .../uncloud/uncloud_vm/views.py | 2 +- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/management/commands/charge-negative-balance.py b/uncloud_django_based/uncloud/uncloud_pay/management/commands/charge-negative-balance.py index 24d53bf..8ee8736 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/management/commands/charge-negative-balance.py +++ b/uncloud_django_based/uncloud/uncloud_pay/management/commands/charge-negative-balance.py @@ -1,6 +1,6 @@ from django.core.management.base import BaseCommand from uncloud_auth.models import User -from uncloud_pay.models import Order, Bill, PaymentMethod, get_balance_for +from uncloud_pay.models import Order, Bill, PaymentMethod, get_balance_for_user from datetime import timedelta from django.utils import timezone @@ -15,7 +15,7 @@ class Command(BaseCommand): users = User.objects.all() print("Processing {} users.".format(users.count())) for user in users: - balance = get_balance_for(user) + balance = get_balance_for_user(user) if balance < 0: print("User {} has negative balance ({}), charging.".format(user.username, balance)) payment_method = PaymentMethod.get_primary_for(user) diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index 59a149c..b26621c 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -92,19 +92,19 @@ class Payment(models.Model): default='unknown') timestamp = models.DateTimeField(editable=False, auto_now_add=True) - # WIP prepaid and service activation logic by fnux. - ## We override save() in order to active products awaiting payment. - #def save(self, *args, **kwargs): - # # TODO: only run activation logic on creation, not on update. - # unpaid_bills_before_payment = Bill.get_unpaid_for(self.owner) - # super(Payment, self).save(*args, **kwargs) # Save payment in DB. - # unpaid_bills_after_payment = Bill.get_unpaid_for(self.owner) + # We override save() in order to active products awaiting payment. + def save(self, *args, **kwargs): + # _state.adding is switched to false after super(...) call. + being_created = self._state.adding - # newly_paid_bills = list( - # set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment)) - # for bill in newly_paid_bills: - # bill.activate_orders() + unpaid_bills_before_payment = Bill.get_unpaid_for(self.owner) + super(Payment, self).save(*args, **kwargs) # Save payment in DB. + unpaid_bills_after_payment = Bill.get_unpaid_for(self.owner) + newly_paid_bills = list( + set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment)) + for bill in newly_paid_bills: + bill.activate_products() class PaymentMethod(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -201,6 +201,12 @@ class Bill(models.Model): valid = models.BooleanField(default=True) + # Trigger product activation if bill paid at creation (from balance). + def save(self, *args, **kwargs): + super(Bill, self).save(*args, **kwargs) + if not self in Bill.get_unpaid_for(self.owner): + self.activate_products() + @property def reference(self): return "{}-{}".format( @@ -227,6 +233,15 @@ class Bill(models.Model): # A bill is final when its ending date is passed. return self.ending_date < timezone.now() + def activate_products(self): + for order in self.order_set.all(): + # FIXME: using __something might not be a good idea. + for product_class in Product.__subclasses__(): + for product in product_class.objects.filter(order=order): + if product.status == UncloudStatus.AWAITING_PAYMENT: + product.status = UncloudStatus.PENDING + product.save() + @staticmethod def generate_for(year, month, user): # /!\ We exclusively work on the specified year and month. @@ -248,7 +263,7 @@ class Bill(models.Model): # (next_bill) ending_date, a new bill has to be generated. # * For yearly bill: if previous_bill.ending_date is on working # month, generate new bill. - unpaid_orders = { 'monthly_or_less': [], 'yearly': {}} + unpaid_orders = { 'monthly_or_less': [], 'yearly': {} } for order in orders: try: previous_bill = order.bill.latest('ending_date') @@ -276,7 +291,7 @@ class Bill(models.Model): else: unpaid_orders['yearly'][next_yearly_bill_start_on] = bucket + [order] else: - if previous_bill == None or previous_bill.ending_date <= ending_date: + if previous_bill == None or previous_bill.ending_date < ending_date: unpaid_orders['monthly_or_less'].append(order) # Handle working month's billing. @@ -335,25 +350,24 @@ class Bill(models.Model): @staticmethod def get_unpaid_for(user): - balance = get_balance_for(user) + balance = get_balance_for_user(user) unpaid_bills = [] # No unpaid bill if balance is positive. if balance >= 0: - return [] + return unpaid_bills else: bills = Bill.objects.filter( owner=user, - due_date__lt=timezone.now() ).order_by('-creation_date') # Amount to be paid by the customer. unpaid_balance = abs(balance) for bill in bills: - if unpaid_balance < 0: + if unpaid_balance <= 0: break - unpaid_balance -= bill.amount - unpaid_bills.append(bill) + unpaid_balance -= bill.total + unpaid_bills.append(bill) return unpaid_bills @@ -464,6 +478,11 @@ class Order(models.Model): choices = RecurringPeriod.choices, default = RecurringPeriod.PER_MONTH) + # Trigger initial bill generation at order creation. + def save(self, *args, **kwargs): + super(Order, self).save(*args, **kwargs) + Bill.generate_for(timezone.now().year, timezone.now().month, self.owner) + @property def records(self): return OrderRecord.objects.filter(order=self) @@ -531,11 +550,11 @@ class Product(UncloudModel): on_delete=models.CASCADE, editable=False) - description = "" + description = "Generic Product" status = models.CharField(max_length=32, choices=UncloudStatus.choices, - default=UncloudStatus.PENDING) + default=UncloudStatus.AWAITING_PAYMENT) order = models.ForeignKey(Order, on_delete=models.CASCADE, diff --git a/uncloud_django_based/uncloud/uncloud_vm/views.py b/uncloud_django_based/uncloud/uncloud_vm/views.py index 50e2e66..71ffe6d 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/views.py +++ b/uncloud_django_based/uncloud/uncloud_vm/views.py @@ -116,7 +116,7 @@ class VMProductViewSet(ProductViewSet): order_recurring_period = serializer.validated_data.pop("recurring_period") # Create base order. - order = Order.objects.create( + order = Order( recurring_period=order_recurring_period, owner=request.user, starting_date=timezone.now() From 83a0ca0e4ee032fe5ee5047062d9062c29a8d903 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Fri, 17 Apr 2020 09:15:52 +0200 Subject: [PATCH 2/7] Adapt billing tests to product activation structure --- uncloud_django_based/uncloud/uncloud_pay/models.py | 6 +++++- uncloud_django_based/uncloud/uncloud_pay/tests.py | 6 +++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index b26621c..b213c35 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -481,7 +481,7 @@ class Order(models.Model): # Trigger initial bill generation at order creation. def save(self, *args, **kwargs): super(Order, self).save(*args, **kwargs) - Bill.generate_for(timezone.now().year, timezone.now().month, self.owner) + Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner) @property def records(self): @@ -496,6 +496,10 @@ class Order(models.Model): return reduce(lambda acc, record: acc + record.recurring_price, self.records, 0) # Used by uncloud_pay tests. + @property + def bills(self): + return Bill.objects.filter(order=self) + def add_record(self, one_time_price, recurring_price, description): OrderRecord.objects.create(order=self, one_time_price=one_time_price, diff --git a/uncloud_django_based/uncloud/uncloud_pay/tests.py b/uncloud_django_based/uncloud/uncloud_pay/tests.py index d441e75..f76007f 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/tests.py +++ b/uncloud_django_based/uncloud/uncloud_pay/tests.py @@ -31,7 +31,7 @@ class BillingTestCase(TestCase): order.add_record(one_time_price, recurring_price, description) # Generate & check bill for first month: full recurring_price + setup. - first_month_bills = Bill.generate_for(2020, 3, self.user) + first_month_bills = order.bills # Initial bill generated at order creation. self.assertEqual(len(first_month_bills), 1) self.assertEqual(first_month_bills[0].total, one_time_price + recurring_price) @@ -65,7 +65,7 @@ class BillingTestCase(TestCase): order.add_record(one_time_price, recurring_price, description) # Generate & check bill for first year: recurring_price + setup. - first_year_bills = Bill.generate_for(2020, 3, self.user) + first_year_bills = order.bills # Initial bill generated at order creation. self.assertEqual(len(first_year_bills), 1) self.assertEqual(first_year_bills[0].starting_date.date(), date.fromisoformat('2020-03-31')) @@ -106,7 +106,7 @@ class BillingTestCase(TestCase): order.add_record(one_time_price, recurring_price, description) # Generate & check bill for first month: recurring_price + setup. - first_month_bills = Bill.generate_for(2020, 3, self.user) + first_month_bills = order.bills self.assertEqual(len(first_month_bills), 1) self.assertEqual(float(first_month_bills[0].total), round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price) From c57780fb4dd818953093c70f507e096427a5181c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Fri, 17 Apr 2020 10:08:33 +0200 Subject: [PATCH 3/7] Add naive GenericServiceProduct --- uncloud_django_based/uncloud/uncloud/urls.py | 2 + .../uncloud/uncloud_service/models.py | 30 ++++++++++- .../uncloud/uncloud_service/serializers.py | 13 ++++- .../uncloud/uncloud_service/views.py | 52 +++++++++++++++++-- .../migrations/0005_auto_20200417_0551.py | 18 +++++++ .../migrations/0006_genericserviceproduct.py | 36 +++++++++++++ 6 files changed, 146 insertions(+), 5 deletions(-) create mode 100644 uncloud_django_based/uncloud/ungleich_service/migrations/0005_auto_20200417_0551.py create mode 100644 uncloud_django_based/uncloud/ungleich_service/migrations/0006_genericserviceproduct.py diff --git a/uncloud_django_based/uncloud/uncloud/urls.py b/uncloud_django_based/uncloud/uncloud/urls.py index 00eaf16..343e06b 100644 --- a/uncloud_django_based/uncloud/uncloud/urls.py +++ b/uncloud_django_based/uncloud/uncloud/urls.py @@ -44,6 +44,8 @@ router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct') # Services router.register(r'service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct') +router.register(r'service/generic', serviceviews.GenericServiceProductViewSet, basename='genericserviceproduct') + # Net router.register(r'net/vpn', netviews.VPNNetworkViewSet, basename='vpnnet') diff --git a/uncloud_django_based/uncloud/uncloud_service/models.py b/uncloud_django_based/uncloud/uncloud_service/models.py index fb1af50..fc92157 100644 --- a/uncloud_django_based/uncloud/uncloud_service/models.py +++ b/uncloud_django_based/uncloud/uncloud_service/models.py @@ -1,8 +1,9 @@ import uuid from django.db import models -from uncloud_pay.models import Product, RecurringPeriod +from uncloud_pay.models import Product, RecurringPeriod, AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS from uncloud_vm.models import VMProduct, VMDiskImageProduct +from django.core.validators import MinValueValidator class MatrixServiceProduct(Product): monthly_managment_fee = 20 @@ -33,3 +34,30 @@ class MatrixServiceProduct(Product): @property def one_time_price(self): return 30 + +class GenericServiceProduct(Product): + custom_description = models.TextField() + custom_recurring_price = models.DecimalField(default=0.0, + max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS, + validators=[MinValueValidator(0)]) + custom_one_time_price = models.DecimalField(default=0.0, + max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS, + validators=[MinValueValidator(0)]) + + @property + def recurring_price(self): + return self.custom_recurring_price + + @property + def description(self): + return self.custom_description + + @property + def one_time_price(self): + return self.custom_one_time_price + + @staticmethod + def allowed_recurring_periods(): + return RecurringPeriod.choices diff --git a/uncloud_django_based/uncloud/uncloud_service/serializers.py b/uncloud_django_based/uncloud/uncloud_service/serializers.py index b4038b7..1d50bbf 100644 --- a/uncloud_django_based/uncloud/uncloud_service/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_service/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import MatrixServiceProduct +from .models import * from uncloud_vm.serializers import ManagedVMProductSerializer from uncloud_vm.models import VMProduct from uncloud_pay.models import RecurringPeriod @@ -15,3 +15,14 @@ class MatrixServiceProductSerializer(serializers.ModelSerializer): model = MatrixServiceProduct fields = ['uuid', 'order', 'owner', 'status', 'vm', 'domain', 'recurring_period'] read_only_fields = ['uuid', 'order', 'owner', 'status'] + +class GenericServiceProductSerializer(serializers.ModelSerializer): + # Custom field used at creation (= ordering) only. + recurring_period = serializers.ChoiceField( + choices=GenericServiceProduct.allowed_recurring_periods()) + + class Meta: + model = GenericServiceProduct + fields = ['uuid', 'order', 'owner', 'status', 'custom_recurring_price', + 'custom_description', 'custom_one_time_price', 'recurring_period'] + read_only_fields = ['uuid', 'order', 'owner', 'status'] diff --git a/uncloud_django_based/uncloud/uncloud_service/views.py b/uncloud_django_based/uncloud/uncloud_service/views.py index e25f3a5..d4be3a6 100644 --- a/uncloud_django_based/uncloud/uncloud_service/views.py +++ b/uncloud_django_based/uncloud/uncloud_service/views.py @@ -1,9 +1,10 @@ from rest_framework import viewsets, permissions from rest_framework.response import Response from django.db import transaction +from django.utils import timezone -from .models import MatrixServiceProduct -from .serializers import MatrixServiceProductSerializer +from .models import * +from .serializers import * from uncloud_pay.helpers import ProductViewSet from uncloud_pay.models import Order @@ -47,7 +48,10 @@ class MatrixServiceProductViewSet(ProductViewSet): # Create base order.) order = Order.objects.create( recurring_period=order_recurring_period, - owner=request.user) + owner=request.user, + starting_date=timezone.now() + ) + order.save() # Create unerderlying VM. data = serializer.validated_data.pop('vm') @@ -65,3 +69,45 @@ class MatrixServiceProductViewSet(ProductViewSet): vm=vm) return Response(serializer.data) + +class GenericServiceProductViewSet(ProductViewSet): + permission_classes = [permissions.IsAuthenticated] + serializer_class = GenericServiceProductSerializer + + def get_queryset(self): + return GenericServiceProduct.objects.filter(owner=self.request.user) + + @transaction.atomic + def create(self, request): + # Extract serializer data. + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + order_recurring_period = serializer.validated_data.pop("recurring_period") + + # Create base order. + order = Order.objects.create( + recurring_period=order_recurring_period, + owner=request.user, + starting_date=timezone.now() + ) + order.save() + + # Create service. + print(serializer.validated_data) + service = serializer.save(order=order, owner=request.user) + + # XXX: Move this to some kind of on_create hook in parent + # Product class? + order.add_record( + service.one_time_price, + service.recurring_price, + service.description) + + # XXX: Move this to some kind of on_create hook in parent + # Product class? + order.add_record( + service.one_time_price, + service.recurring_price, + service.description) + + return Response(serializer.data) diff --git a/uncloud_django_based/uncloud/ungleich_service/migrations/0005_auto_20200417_0551.py b/uncloud_django_based/uncloud/ungleich_service/migrations/0005_auto_20200417_0551.py new file mode 100644 index 0000000..aed07b6 --- /dev/null +++ b/uncloud_django_based/uncloud/ungleich_service/migrations/0005_auto_20200417_0551.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-17 05:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ungleich_service', '0004_auto_20200403_1727'), + ] + + operations = [ + migrations.AlterField( + model_name='matrixserviceproduct', + name='status', + field=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), + ), + ] diff --git a/uncloud_django_based/uncloud/ungleich_service/migrations/0006_genericserviceproduct.py b/uncloud_django_based/uncloud/ungleich_service/migrations/0006_genericserviceproduct.py new file mode 100644 index 0000000..f4bda32 --- /dev/null +++ b/uncloud_django_based/uncloud/ungleich_service/migrations/0006_genericserviceproduct.py @@ -0,0 +1,36 @@ +# Generated by Django 3.0.5 on 2020-04-17 08:02 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0005_auto_20200417_0551'), + ('ungleich_service', '0005_auto_20200417_0551'), + ] + + operations = [ + migrations.CreateModel( + name='GenericServiceProduct', + fields=[ + ('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('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)), + ('custom_description', models.TextField()), + ('custom_recurring_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('custom_one_time_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('order', models.ForeignKey(editable=False, 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, + }, + ), + ] From d1e993140c9afc61503ab96fe71a2721b6be9586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Fri, 17 Apr 2020 10:09:28 +0200 Subject: [PATCH 4/7] Add simple product activation test --- .../uncloud/uncloud_pay/tests.py | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/tests.py b/uncloud_django_based/uncloud/uncloud_pay/tests.py index f76007f..4bdd791 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/tests.py +++ b/uncloud_django_based/uncloud/uncloud_pay/tests.py @@ -3,6 +3,7 @@ from django.contrib.auth import get_user_model from datetime import datetime, date, timedelta from .models import * +from ungleich_service.models import GenericServiceProduct class BillingTestCase(TestCase): def setUp(self): @@ -10,9 +11,6 @@ class BillingTestCase(TestCase): username='jdoe', email='john.doe@domain.tld') - def test_truth(self): - self.assertEqual(1+1, 2) - def test_basic_monthly_billing(self): one_time_price = 10 recurring_price = 20 @@ -116,3 +114,41 @@ class BillingTestCase(TestCase): self.assertEqual(len(second_month_bills), 1) self.assertEqual(float(second_month_bills[0].total), round(12 * recurring_price, AMOUNT_DECIMALS)) + +class ProductActivationTestCase(TestCase): + def setUp(self): + self.user = get_user_model().objects.create( + username='jdoe', + email='john.doe@domain.tld') + + def test_product_activation(self): + starting_date = datetime.fromisoformat('2020-03-01') + + order = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH) + order.save() + + product = GenericServiceProduct( + custom_description="Test product", + custom_one_time_price=0, + custom_recurring_price=20, + owner=self.user, + order=order) + product.save() + + # XXX: to be automated. + order.add_record(product.one_time_price, product.recurring_price, product.description) + + # Validate initial state: must be awaiting payment. + self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT) + + # Pay initial bill, check that product is activated. + amount = product.order.bills[0].total + payment = Payment(owner=self.user, amount=amount) + payment.save() + self.assertEqual( + GenericServiceProduct.objects.get(uuid=product.uuid).status, + UncloudStatus.PENDING + ) From b6c976b722206f60b73b25b7612cbc7f1bc27fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Fri, 17 Apr 2020 10:18:50 +0200 Subject: [PATCH 5/7] Commit autp-generated migrations (missing from master?) --- .../migrations/0003_auto_20200417_0551.py | 18 +++++++++++++++ .../migrations/0005_auto_20200417_0551.py | 18 +++++++++++++++ .../migrations/0009_auto_20200417_0551.py | 23 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 uncloud_django_based/uncloud/uncloud_net/migrations/0003_auto_20200417_0551.py create mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200417_0551.py create mode 100644 uncloud_django_based/uncloud/uncloud_vm/migrations/0009_auto_20200417_0551.py diff --git a/uncloud_django_based/uncloud/uncloud_net/migrations/0003_auto_20200417_0551.py b/uncloud_django_based/uncloud/uncloud_net/migrations/0003_auto_20200417_0551.py new file mode 100644 index 0000000..24f4a7f --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_net/migrations/0003_auto_20200417_0551.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-17 05:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_net', '0002_auto_20200409_1225'), + ] + + operations = [ + migrations.AlterField( + model_name='vpnnetwork', + name='status', + field=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), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200417_0551.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200417_0551.py new file mode 100644 index 0000000..fa27e03 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200417_0551.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-17 05:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0004_auto_20200409_1225'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='recurring_period', + field=models.CharField(choices=[('ONCE', 'Onetime'), ('YEAR', 'Per Year'), ('MONTH', 'Per Month'), ('WEEK', 'Per Week'), ('DAY', 'Per Day'), ('HOUR', 'Per Hour'), ('MINUTE', 'Per Minute'), ('SECOND', 'Per Second')], default='MONTH', max_length=32), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_auto_20200417_0551.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_auto_20200417_0551.py new file mode 100644 index 0000000..641f849 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_auto_20200417_0551.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.5 on 2020-04-17 05:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0008_auto_20200403_1727'), + ] + + operations = [ + migrations.AlterField( + model_name='vmproduct', + name='status', + field=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), + ), + migrations.AlterField( + model_name='vmsnapshotproduct', + name='status', + field=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), + ), + ] From 83d2cd465d5cdb1f9e5acfa685abe7f84c7bbfde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 08:42:50 +0200 Subject: [PATCH 6/7] Sync migrations after rebase --- .../migrations/0005_auto_20200417_0551.py | 18 -------- .../migrations/0002_auto_20200418_0641.py | 41 +++++++++++++++++++ .../migrations/0004_vmproduct_primary_disk.py | 2 +- .../migrations/0011_merge_20200418_0641.py | 14 +++++++ .../migrations/0012_auto_20200418_0641.py | 18 ++++++++ 5 files changed, 74 insertions(+), 19 deletions(-) delete mode 100644 uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200417_0551.py create mode 100644 uncloud_django_based/uncloud/uncloud_service/migrations/0002_auto_20200418_0641.py create mode 100644 uncloud_django_based/uncloud/uncloud_vm/migrations/0011_merge_20200418_0641.py create mode 100644 uncloud_django_based/uncloud/uncloud_vm/migrations/0012_auto_20200418_0641.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200417_0551.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200417_0551.py deleted file mode 100644 index fa27e03..0000000 --- a/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200417_0551.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.0.5 on 2020-04-17 05:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0004_auto_20200409_1225'), - ] - - operations = [ - migrations.AlterField( - model_name='order', - name='recurring_period', - field=models.CharField(choices=[('ONCE', 'Onetime'), ('YEAR', 'Per Year'), ('MONTH', 'Per Month'), ('WEEK', 'Per Week'), ('DAY', 'Per Day'), ('HOUR', 'Per Hour'), ('MINUTE', 'Per Minute'), ('SECOND', 'Per Second')], default='MONTH', max_length=32), - ), - ] diff --git a/uncloud_django_based/uncloud/uncloud_service/migrations/0002_auto_20200418_0641.py b/uncloud_django_based/uncloud/uncloud_service/migrations/0002_auto_20200418_0641.py new file mode 100644 index 0000000..717f163 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_service/migrations/0002_auto_20200418_0641.py @@ -0,0 +1,41 @@ +# Generated by Django 3.0.5 on 2020-04-18 06:41 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0005_auto_20200413_0924'), + ('uncloud_service', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='matrixserviceproduct', + name='status', + field=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), + ), + migrations.CreateModel( + name='GenericServiceProduct', + fields=[ + ('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('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)), + ('custom_description', models.TextField()), + ('custom_recurring_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('custom_one_time_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('order', models.ForeignKey(editable=False, 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_django_based/uncloud/uncloud_vm/migrations/0004_vmproduct_primary_disk.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0004_vmproduct_primary_disk.py index 90c4e33..c78acc1 100644 --- a/uncloud_django_based/uncloud/uncloud_vm/migrations/0004_vmproduct_primary_disk.py +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0004_vmproduct_primary_disk.py @@ -7,7 +7,7 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('uncloud_vm', '0003_remove_vmhost_vms'), + ('uncloud_vm', '0004_remove_vmproduct_vmid'), ] operations = [ diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0011_merge_20200418_0641.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0011_merge_20200418_0641.py new file mode 100644 index 0000000..c0d4c32 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0011_merge_20200418_0641.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.5 on 2020-04-18 06:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0009_auto_20200417_0551'), + ('uncloud_vm', '0010_auto_20200413_0924'), + ] + + operations = [ + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0012_auto_20200418_0641.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0012_auto_20200418_0641.py new file mode 100644 index 0000000..9af8649 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0012_auto_20200418_0641.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-18 06:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0011_merge_20200418_0641'), + ] + + operations = [ + migrations.AlterField( + model_name='vmdiskproduct', + name='status', + field=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), + ), + ] From 86775af4c88d0342ee5a24b9ff941e5b88d0ba77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Floure?= Date: Sat, 18 Apr 2020 09:02:33 +0200 Subject: [PATCH 7/7] Fix product activation tests after rebase --- uncloud_django_based/uncloud/uncloud_pay/models.py | 2 +- uncloud_django_based/uncloud/uncloud_pay/tests.py | 4 ++-- uncloud_django_based/uncloud/uncloud_service/models.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py index b213c35..4cb1952 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/models.py +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -579,7 +579,7 @@ class Product(UncloudModel): if being_created: record = OrderRecord( one_time_price=self.one_time_price, - recurring_price=self.recurring_price(self.recurring_period), + recurring_price=self.recurring_price(recurring_period=self.recurring_period), description=self.description) self.order.orderrecord_set.add(record, bulk=False) diff --git a/uncloud_django_based/uncloud/uncloud_pay/tests.py b/uncloud_django_based/uncloud/uncloud_pay/tests.py index 4bdd791..9e8728d 100644 --- a/uncloud_django_based/uncloud/uncloud_pay/tests.py +++ b/uncloud_django_based/uncloud/uncloud_pay/tests.py @@ -3,7 +3,7 @@ from django.contrib.auth import get_user_model from datetime import datetime, date, timedelta from .models import * -from ungleich_service.models import GenericServiceProduct +from uncloud_service.models import GenericServiceProduct class BillingTestCase(TestCase): def setUp(self): @@ -139,7 +139,7 @@ class ProductActivationTestCase(TestCase): product.save() # XXX: to be automated. - order.add_record(product.one_time_price, product.recurring_price, product.description) + order.add_record(product.one_time_price, product.recurring_price(), product.description) # Validate initial state: must be awaiting payment. self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT) diff --git a/uncloud_django_based/uncloud/uncloud_service/models.py b/uncloud_django_based/uncloud/uncloud_service/models.py index fc92157..26bedfd 100644 --- a/uncloud_django_based/uncloud/uncloud_service/models.py +++ b/uncloud_django_based/uncloud/uncloud_service/models.py @@ -17,7 +17,7 @@ class MatrixServiceProduct(Product): domain = models.CharField(max_length=255, default='domain.tld') # Default recurring price is PER_MONT, see Product class. - def recurring_price(self): + def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH): return self.monthly_managment_fee @staticmethod @@ -46,8 +46,8 @@ class GenericServiceProduct(Product): decimal_places=AMOUNT_DECIMALS, validators=[MinValueValidator(0)]) - @property - def recurring_price(self): + def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH): + # FIXME: handle recurring_period somehow. return self.custom_recurring_price @property