From 992c7c551eea2932d8bfe456adace2abb43675d6 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Tue, 6 Oct 2020 15:46:22 +0200 Subject: [PATCH] Make recurring period a database model - For easier handling (foreignkeys, many2many) - For higher flexibility (users can define their own periods) --- .../management/commands/db-add-defaults.py | 11 + uncloud_net/models.py | 2 +- uncloud_pay/admin.py | 7 +- .../migrations/0027_auto_20201006_1319.py | 41 ++ uncloud_pay/models.py | 166 +++-- uncloud_pay/serializers.py | 2 +- uncloud_pay/tests.py | 679 +++++++++++------- uncloud_service/models.py | 14 +- uncloud_service/serializers.py | 8 +- uncloud_vm/models.py | 12 +- uncloud_vm/serializers.py | 8 +- 11 files changed, 588 insertions(+), 362 deletions(-) create mode 100644 uncloud/management/commands/db-add-defaults.py create mode 100644 uncloud_pay/migrations/0027_auto_20201006_1319.py diff --git a/uncloud/management/commands/db-add-defaults.py b/uncloud/management/commands/db-add-defaults.py new file mode 100644 index 0000000..28e29c1 --- /dev/null +++ b/uncloud/management/commands/db-add-defaults.py @@ -0,0 +1,11 @@ +from django.core.management.base import BaseCommand +from uncloud_pay.models import RecurringPeriod + +class Command(BaseCommand): + help = 'Add standard uncloud values' + + def add_arguments(self, parser): + pass + + def handle(self, *args, **options): + RecurringPeriod.populate_db_defaults() diff --git a/uncloud_net/models.py b/uncloud_net/models.py index 153b456..47990d8 100644 --- a/uncloud_net/models.py +++ b/uncloud_net/models.py @@ -173,7 +173,7 @@ class VPNNetwork(models.Model): wireguard_public_key = models.CharField(max_length=48) - default_recurring_period = RecurringPeriod.PER_365D +# default_recurring_period = RecurringPeriod.PER_365D @property def recurring_price(self): diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py index 14fc00f..3154ae5 100644 --- a/uncloud_pay/admin.py +++ b/uncloud_pay/admin.py @@ -11,7 +11,7 @@ from django.http import FileResponse from django.template.loader import render_to_string -from uncloud_pay.models import Bill, Order, BillRecord, BillingAddress, Product +from uncloud_pay.models import Bill, Order, BillRecord, BillingAddress, Product, RecurringPeriod class BillRecordInline(admin.TabularInline): @@ -90,9 +90,8 @@ admin.site.register(Bill, BillAdmin) admin.site.register(Order) admin.site.register(BillRecord) admin.site.register(BillingAddress) - -#for m in [ SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee ]: - +admin.site.register(RecurringPeriod) admin.site.register(Product) #admin.site.register(Order, OrderAdmin) +#for m in [ SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee ]: diff --git a/uncloud_pay/migrations/0027_auto_20201006_1319.py b/uncloud_pay/migrations/0027_auto_20201006_1319.py new file mode 100644 index 0000000..a82955a --- /dev/null +++ b/uncloud_pay/migrations/0027_auto_20201006_1319.py @@ -0,0 +1,41 @@ +# Generated by Django 3.1 on 2020-10-06 13:19 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0026_order_should_be_billed'), + ] + + operations = [ + migrations.CreateModel( + name='RecurringPeriod', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('duration_seconds', models.IntegerField(unique=True)), + ], + ), + migrations.DeleteModel( + name='SampleOneTimeProduct', + ), + migrations.DeleteModel( + name='SampleRecurringProduct', + ), + migrations.DeleteModel( + name='SampleRecurringProductOneTimeFee', + ), + migrations.AlterField( + model_name='order', + name='recurring_period', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.recurringperiod'), + ), + migrations.AlterField( + model_name='product', + name='default_recurring_period', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.recurringperiod'), + ), + ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 5f90b02..261313a 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -64,21 +64,6 @@ def start_after(a_date): def default_payment_delay(): return timezone.now() + BILL_PAYMENT_DELAY -# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types -class RecurringPeriod(models.IntegerChoices): - """ - We don't support months are years, because they vary in length. - This is not only complicated, but also unfair to the user, as the user pays the same - amount for different durations. - """ - PER_365D = 365*24*3600, _('Per 365 days') - PER_30D = 30*24*3600, _('Per 30 days') - PER_WEEK = 7*24*3600, _('Per Week') - PER_DAY = 24*3600, _('Per Day') - PER_HOUR = 3600, _('Per Hour') - PER_MINUTE = 60, _('Per Minute') - PER_SECOND = 1, _('Per Second') - ONE_TIME = 0, _('Onetime') class Currency(models.TextChoices): """ @@ -236,6 +221,41 @@ class PaymentMethod(models.Model): # non-primary method. pass +# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types +class RecurringPeriodChoices(models.IntegerChoices): + """ + This is an old class and being superseeded by the database model below + """ + PER_365D = 365*24*3600, _('Per 365 days') + PER_30D = 30*24*3600, _('Per 30 days') + PER_WEEK = 7*24*3600, _('Per Week') + PER_DAY = 24*3600, _('Per Day') + PER_HOUR = 3600, _('Per Hour') + PER_MINUTE = 60, _('Per Minute') + PER_SECOND = 1, _('Per Second') + ONE_TIME = 0, _('Onetime') + +# RecurringPeriods +class RecurringPeriod(models.Model): + """ + Available recurring periods. + By default seeded from RecurringPeriodChoices + """ + + name = models.CharField(max_length=100, unique=True) + duration_seconds = models.IntegerField(unique=True) + + @classmethod + def populate_db_defaults(cls): + for (seconds, name) in RecurringPeriodChoices.choices: + obj, created = cls.objects.get_or_create(name=name, + defaults={ 'duration_seconds': seconds }) + + + def __str__(self): + return f"{self.name} ({self.duration_seconds})" + + ### # Bills. @@ -313,7 +333,11 @@ class Product(UncloudModel): description = models.CharField(max_length=1024) config = models.JSONField() - default_recurring_period = models.IntegerField(choices=RecurringPeriod.choices, default=RecurringPeriod.PER_30D) + +# default_recurring_period = models.IntegerField(choices=RecurringPeriod.choices, default=RecurringPeriod.PER_30D) + default_recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE, editable=True) + + currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF) @property @@ -409,7 +433,6 @@ class Product(UncloudModel): self.create_order(when_to_start, recurring_period) - @property def recurring_price(self): """ implement correct values in the child class """ @@ -564,8 +587,9 @@ class Order(models.Model): ending_date = models.DateTimeField(blank=True, null=True) # FIXME: ensure the period is defined in the product - recurring_period = models.IntegerField(choices = RecurringPeriod.choices, - default = RecurringPeriod.PER_30D) +# recurring_period = models.IntegerField(choices = RecurringPeriod.choices, +# default = RecurringPeriod.PER_30D) + recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE, editable=True) one_time_price = models.DecimalField(default=0.0, max_digits=AMOUNT_MAX_DIGITS, @@ -591,7 +615,6 @@ class Order(models.Model): blank=True, null=True) - should_be_billed = models.BooleanField(default=True) @property @@ -700,6 +723,29 @@ class Order(models.Model): self.ending_date = end_before(new_order.starting_date) self.save() + def update_order(self, config, starting_date=None): + """ + Updating an order means creating a new order and reference the previous order + """ + + if not starting_date: + starting_date = timezone.now() + + new_order = self.__class__(owner=self.owner, + billing_address=self.billing_address, + product=self.product, + starting_date=starting_date, + config=config) + + (new_order_one_time_price, new_order_recurring_price) = new_order.prices + + new_order.replaces = self + new_order.save() + + self.ending_date = end_before(new_order.starting_date) + self.save() + + def save(self, *args, **kwargs): if self.ending_date and self.ending_date < self.starting_date: raise ValidationError("End date cannot be before starting date") @@ -778,12 +824,14 @@ class Order(models.Model): is_recurring_record=True) - def save(self, *args, **kwargs): + @property + def prices(self): one_time_price = 0 recurring_price = 0 # FIXME: support amount independent one time prices # FIXME: support a base price + # FIXME: adjust to the selected recurring_period if 'features' in self.product.config: for feature in self.product.config['features']: @@ -794,12 +842,14 @@ class Order(models.Model): one_time_price += self.product.config['features'][feature]['one_time_price'] * self.config['features'][feature] recurring_price += self.product.config['features'][feature]['recurring_price'] * self.config['features'][feature] + return (one_time_price, recurring_price) + def save(self, *args, **kwargs): + # Calculate the price of the order when we create it # IMMUTABLE fields -- need to create new order to modify them # However this is not enforced here... if self._state.adding: - self.one_time_price = one_time_price - self.recurring_price = recurring_price + (self.one_time_price, self.recurring_price) = self.prices super().save(*args, **kwargs) @@ -1137,50 +1187,50 @@ class BillRecord(models.Model): super().save(*args, **kwargs) -# Sample products included into uncloud -class SampleOneTimeProduct(models.Model): - """ - Products are usually more complex, but this product shows how easy - it can be to create your own one time product. - """ +# # Sample products included into uncloud +# class SampleOneTimeProduct(models.Model): +# """ +# Products are usually more complex, but this product shows how easy +# it can be to create your own one time product. +# """ - default_recurring_period = RecurringPeriod.ONE_TIME +# default_recurring_period = RecurringPeriod.ONE_TIME - ot_price = models.IntegerField(default=5) +# ot_price = models.IntegerField(default=5) - @property - def one_time_price(self): - return self.ot_price +# @property +# def one_time_price(self): +# return self.ot_price -class SampleRecurringProduct(models.Model): - """ - Products are usually more complex, but this product shows how easy - it can be to create your own recurring fee product. - """ +# class SampleRecurringProduct(models.Model): +# """ +# Products are usually more complex, but this product shows how easy +# it can be to create your own recurring fee product. +# """ - default_recurring_period = RecurringPeriod.PER_30D +# default_recurring_period = RecurringPeriod.PER_30D - rc_price = models.IntegerField(default=10) +# rc_price = models.IntegerField(default=10) - @property - def recurring_price(self): - return self.rc_price +# @property +# def recurring_price(self): +# return self.rc_price -class SampleRecurringProductOneTimeFee(models.Model): - """ - Products are usually more complex, but this product shows how easy - it can be to create your own one time + recurring fee product. - """ +# class SampleRecurringProductOneTimeFee(models.Model): +# """ +# Products are usually more complex, but this product shows how easy +# it can be to create your own one time + recurring fee product. +# """ - default_recurring_period = RecurringPeriod.PER_30D +# default_recurring_period = RecurringPeriod.PER_30D - ot_price = models.IntegerField(default=5) - rc_price = models.IntegerField(default=10) +# ot_price = models.IntegerField(default=5) +# rc_price = models.IntegerField(default=10) - @property - def one_time_price(self): - return self.ot_price +# @property +# def one_time_price(self): +# return self.ot_price - @property - def recurring_price(self): - return self.rc_price +# @property +# def recurring_price(self): +# return self.rc_price diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py index e00541c..9214105 100644 --- a/uncloud_pay/serializers.py +++ b/uncloud_pay/serializers.py @@ -82,7 +82,7 @@ class BillRecordSerializer(serializers.Serializer): description = serializers.CharField() one_time_price = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) recurring_price = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) - recurring_period = serializers.ChoiceField(choices=RecurringPeriod.choices) +# recurring_period = serializers.ChoiceField() recurring_count = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) vat_rate = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) vat_amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index dc02232..c78fa8e 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -27,7 +27,7 @@ chocolate_order_config = { chocolate_one_time_price = chocolate_order_config['features']['gramm'] * chocolate_product_config['features']['gramm']['one_time_price'] -vm_sample_product_config = { +vm_product_config = { 'features': { 'cores': { 'min': 1, @@ -44,13 +44,28 @@ vm_sample_product_config = { }, } -vm_sample_order_config = { +vm_order_config = { 'features': { 'cores': 2, 'ram_gb': 2 } } +vm_order_downgrade_config = { + 'features': { + 'cores': 1, + 'ram_gb': 1 + } +} + +vm_order_upgrade_config = { + 'features': { + 'cores': 4, + 'ram_gb': 4 + } +} + + class ProductTestCase(TestCase): """ @@ -77,7 +92,7 @@ class ProductTestCase(TestCase): p = Product.objects.create(name="Testproduct", description="Only for testing", - config=vm_sample_product_config) + config=vm_product_config) class OrderTestCase(TestCase): @@ -105,16 +120,287 @@ class OrderTestCase(TestCase): p = Product.objects.create(name="Testproduct", description="Only for testing", - config=vm_sample_product_config) + config=vm_product_config) o = Order.objects.create(owner=self.user, billing_address=self.ba, product=p, - config=vm_sample_order_config) + config=vm_order_config) self.assertEqual(o.one_time_price, 0) self.assertEqual(o.recurring_price, 16) + def test_change_order(self): + """ + Change an order and ensure that + - a new order is created + - the price is correct in the new order + """ + + p = Product.objects.create(name="Testproduct", + description="Only for testing", + config=vm_product_config) + + order1 = Order.objects.create(owner=self.user, + billing_address=self.ba, + product=p, + config=vm_order_config) + + + + self.assertEqual(order1.one_time_price, 0) + self.assertEqual(order1.recurring_price, 16) + + +class ModifyOrderTestCase(TestCase): + """ + Test typical order flows like + - cancelling + - downgrading + - upgrading + """ + + def setUp(self): + self.user = get_user_model().objects.create( + username='random_user', + email='jane.random@domain.tld') + + self.ba = BillingAddress.objects.create( + owner=self.user, + organization = 'Test org', + street="unknown", + city="unknown", + postal_code="somewhere else", + active=True) + + self.product = Product.objects.create(name="Testproduct", + description="Only for testing", + config=vm_product_config) + + + def test_change_order(self): + """ + Test changing an order + + Expected result: + + - Old order should be closed before new order starts + - New order should start at starting data + """ + + user = self.user + + starting_price = 16 + downgrade_price = 8 + + starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) + ending1_date = starting_date + datetime.timedelta(days=15) + change1_date = start_after(ending1_date) + + bill_ending_date = change1_date + datetime.timedelta(days=1) + + + order1 = Order.objects.create(owner=self.user, + billing_address=BillingAddress.get_address_for(self.user), + product=self.product, + config=vm_order_config, + starting_date=starting_date) + + order1.update_order(vm_order_downgrade_config, starting_date=change1_date) + + bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) + + bill = bills[0] + bill_records = BillRecord.objects.filter(bill=bill) + + self.assertEqual(len(bill_records), 2) + + self.assertEqual(bill_records[0].starting_date, starting_date) + self.assertEqual(bill_records[0].ending_date, ending1_date) + + self.assertEqual(bill_records[1].starting_date, change1_date) + + + +# def test_no_pro_rata_first_bill(self): +# """ +# The bill should NOT contain a partial amount -- this is a BILL TEST :-) +# """ + +# price = 5 + +# # Standard 30d recurring product +# product = SampleRecurringProduct.objects.create(owner=self.user, +# rc_price=price) + +# starting_date = timezone.make_aware(datetime.datetime(2020,3,3)) +# ending_date = timezone.make_aware(datetime.datetime(2020,3,31)) +# time_diff = (ending_date - starting_date).total_seconds() + +# product.create_order(starting_date) + +# bills = Bill.create_next_bills_for_user(self.user, +# ending_date=ending_date) + + +# # We expect 1 bill for 1 billing address and 1 time frame +# self.assertEqual(len(bills), 1) + +# pro_rata_amount = time_diff / product.default_recurring_period.value + +# self.assertNotEqual(bills[0].sum, pro_rata_amount * price) +# self.assertEqual(bills[0].sum, price) + + + def test_downgrade_product(self): + """ + Test downgrading behaviour: + + We create a recurring product (recurring time: 30 days) and downgrade after 15 days. + + We create the bill right AFTER the end of the first order. + + Expected result: + + - First bill record for 30 days + - Second bill record starting after 30 days + - Bill contains two bill records + + """ + + user = self.user + + starting_price = 16 + downgrade_price = 8 + + starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) + first_order_should_end_at = starting_date + datetime.timedelta(days=30) + change1_date = start_after(starting_date + datetime.timedelta(days=15)) + bill_ending_date = change1_date + datetime.timedelta(days=1) + + + order1 = Order.objects.create(owner=self.user, + billing_address=BillingAddress.get_address_for(self.user), + product=self.product, + config=vm_order_config, + starting_date=starting_date) + + order1.update_order(vm_order_downgrade_config, starting_date=change1_date) + + # product = Product.objects.create(owner=user, rc_price=starting_price) + # product.create_order(starting_date) + # product.rc_price = downgrade_price + # product.save() + # product.create_or_update_recurring_order(when_to_start=change1_date) + + bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) + + bill = bills[0] + bill_records = BillRecord.objects.filter(bill=bill) + + self.assertEqual(len(bill_records), 2) + + self.assertEqual(bill_records[0].starting_date, starting_date) + self.assertEqual(bill_records[0].order.ending_date, first_order_should_end_at) + + # self.assertEqual(bill_records[0].ending_date, first_order_should_end_at) + + # self.assertEqual(bill_records[0].quantity, 1) + + # self.assertEqual(bill_records[1].quantity, 1) + # self.assertEqual(int(bill.sum), 15) + +# def test_upgrade_product(self): +# """ +# Test upgrading behaviour +# """ + +# user = self.user + +# # Create product +# starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) +# starting_price = 10 +# product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) +# product.create_order(starting_date) + +# change1_date = start_after(starting_date + datetime.timedelta(days=15)) +# product.rc_price = 20 +# product.save() +# product.create_or_update_recurring_order(when_to_start=change1_date) + +# bill_ending_date = change1_date + datetime.timedelta(days=1) +# bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) + +# bill = bills[0] +# bill_records = BillRecord.objects.filter(bill=bill) + +# self.assertEqual(len(bill_records), 2) +# self.assertEqual(bill_records[0].quantity, .5) + +# self.assertEqual(bill_records[0].ending_date, end_before(change1_date)) + +# self.assertEqual(bill_records[1].quantity, 1) +# self.assertEqual(bill_records[1].starting_date, change1_date) + +# self.assertEqual(int(bill.sum), 25) + + + + # def test_bill_for_increasing_product(self): + # """ + # Modify a product, see one pro rata entry + # """ + + # # Create product + # starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) + # starting_price = 30.5 + # product = SampleRecurringProduct.objects.create(owner=self.user, + # rc_price=starting_price) + # product.create_order(starting_date) + + # recurring_period = product.default_recurring_period.value + + # # First change + # change1_date = timezone.make_aware(datetime.datetime(2019,4,17)) + # product.rc_price = 49.5 + # product.save() + # product.create_or_update_recurring_order(when_to_start=change1_date) + + # # Second change + # change2_date = timezone.make_aware(datetime.datetime(2019,5,8)) + # product.rc_price = 56.5 + # product.save() + # product.create_or_update_recurring_order(when_to_start=change2_date) + + # # Create bill one month after 2nd change + # bill_ending_date = timezone.make_aware(datetime.datetime(2019,6,30)) + # bills = Bill.create_next_bills_for_user(self.user, + # ending_date=bill_ending_date) + + # # only one bill in this test case + # bill = bills[0] + + # expected_amount = starting_price + + # d2 = starting_date + recurring_period + # duration2 = change1_date - d2 + + # expected_amount = 0 + + # # Expected bill sum & records: + # # 2019-03-03 - 2019-04-02 +30d: 30.5 + # # 2019-04-02 - 2019-04-17: +15d: 15.25 + # # 2019-04-17 - 2019-05-08: +21d: (21/30) * 49.5 + # # 2019-05-08 - 2019-06-07: +30d: 56.5 + # # 2019-06-07 - 2019-07-07: +30d: 56.5 + + + # self.assertEqual(bills[0].sum, price) + + # # expeted result: + # # 1x 5 chf bill record + # # 1x 5 chf bill record + # # 1x 10 partial bill record class BillTestCase(TestCase): @@ -163,14 +449,11 @@ class BillTestCase(TestCase): description="Not only for testing, but for joy", config=chocolate_product_config) - # self.recurring_order = Order.objects.create( - # owner=self.recurring_user, - # starting_date=timezone.make_aware(datetime.datetime(2020,3,3)), - # recurring_period=RecurringPeriod.PER_30D, - # price=15, - # description="A pretty VM", - # billing_address=BillingAddress.get_address_for(self.recurring_user) - # ) + + self.vm = Product.objects.create(name="Super Fast VM", + description="Zooooom", + config=vm_product_config) + # used for generating multiple bills self.bill_dates = [ @@ -190,6 +473,28 @@ class BillTestCase(TestCase): ending_date=self.order_meta[1]['ending_date'], config=chocolate_order_config) + def order_vm(self, owner=None): + + if not owner: + owner = self.recurring_user + + return Order.objects.create( + owner=owner, + product=self.vm, + config=vm_order_config, + billing_address=BillingAddress.get_address_for(self.recurring_user), + starting_date=timezone.make_aware(datetime.datetime(2020,3,3)), + ) + + return Order.objects.create( + owner=self.user, + recurring_period=RecurringPeriod.ONE_TIME, + product=self.chocolate, + billing_address=BillingAddress.get_address_for(self.user), + starting_date=self.order_meta[1]['starting_date'], + ending_date=self.order_meta[1]['ending_date'], + config=chocolate_order_config) + def test_bill_one_time_one_bill_record(self): @@ -213,83 +518,95 @@ class BillTestCase(TestCase): self.assertEqual(bill.sum, chocolate_one_time_price) -# def test_bill_creates_record_for_recurring_order(self): -# """ -# Ensure there is only 1 bill record per order -# """ + def test_bill_creates_record_for_recurring_order(self): + """ + Ensure there is only 1 bill record per order + """ -# bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr) + order = self.order_vm() + bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr) -# self.assertEqual(self.recurring_order.billrecord_set.count(), 1) -# self.assertEqual(bill.billrecord_set.count(), 1) + self.assertEqual(order.billrecord_set.count(), 1) + self.assertEqual(bill.billrecord_set.count(), 1) -# def test_new_bill_after_closing(self): -# """ -# After closing a bill and the user has a recurring product, -# the next bill run should create e new bill -# """ + def test_new_bill_after_closing(self): + """ + After closing a bill and the user has a recurring product, + the next bill run should create e new bill + """ -# for ending_date in self.bill_dates: -# b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date) -# b.close() + order = self.order_vm() -# bill_count = Bill.objects.filter(owner=self.recurring_user).count() + for ending_date in self.bill_dates: + b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date) + b.close() -# self.assertEqual(len(self.bill_dates), bill_count) + bill_count = Bill.objects.filter(owner=self.recurring_user).count() -# def test_multi_addr_multi_bill(self): -# """ -# Ensure multiple bills are created if orders exist with different billing addresses -# """ + self.assertEqual(len(self.bill_dates), bill_count) -# username="lotsofplaces" -# multi_addr_user = get_user_model().objects.create( -# username=username, -# email=f"{username}@example.org") + # def test_multi_addr_multi_bill(self): + # """ + # Ensure multiple bills are created if orders exist with different billing addresses + # """ -# user_addr1 = BillingAddress.objects.create( -# owner=multi_addr_user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="unknown", -# active=True) + # username="lotsofplaces" + # multi_addr_user = get_user_model().objects.create( + # username=username, + # email=f"{username}@example.org") -# order1 = Order.objects.create( -# owner=multi_addr_user, -# starting_date=self.order_meta[1]['starting_date'], -# ending_date=self.order_meta[1]['ending_date'], -# recurring_period=RecurringPeriod.ONE_TIME, -# price=self.order_meta[1]['price'], -# description=self.order_meta[1]['description'], -# billing_address=BillingAddress.get_address_for(self.user)) + # user_addr1 = BillingAddress.objects.create( + # owner=multi_addr_user, + # organization = 'Test org', + # street="unknown", + # city="unknown", + # postal_code="unknown", + # active=True) -# # Make this address inactive -# user_addr1.active = False -# user_addr1.save() + # order1 = Order.objects.create( + # owner=multi_addr_user, + # recurring_period=RecurringPeriod.ONE_TIME, + # product=self.chocolate, + # billing_address=BillingAddress.get_address_for(self.user), + # starting_date=self.order_meta[1]['starting_date'], + # ending_date=self.order_meta[1]['ending_date'], + # config=chocolate_order_config) -# user_addr2 = BillingAddress.objects.create( -# owner=multi_addr_user, -# organization = 'Test2 org', -# street="unknown2", -# city="unknown2", -# postal_code="unknown2", -# active=True) + # order1 = Order.objects.create( + # owner=multi_addr_user, + # starting_date=self.order_meta[1]['starting_date'], + # ending_date=self.order_meta[1]['ending_date'], + # recurring_period=RecurringPeriod.ONE_TIME, + # price=self.order_meta[1]['price'], + # description=self.order_meta[1]['description'], + # billing_address=BillingAddress.get_address_for(self.user)) -# order2 = Order.objects.create( -# owner=multi_addr_user, -# starting_date=self.order_meta[1]['starting_date'], -# ending_date=self.order_meta[1]['ending_date'], -# recurring_period=RecurringPeriod.ONE_TIME, -# price=self.order_meta[1]['price'], -# description=self.order_meta[1]['description'], -# billing_address=BillingAddress.get_address_for(self.user)) + # # Make this address inactive + # user_addr1.active = False + # user_addr1.save() + + # user_addr2 = BillingAddress.objects.create( + # owner=multi_addr_user, + # organization = 'Test2 org', + # street="unknown2", + # city="unknown2", + # postal_code="unknown2", + # active=True) + + # order2 = Order.objects.create( + # owner=multi_addr_user, + # starting_date=self.order_meta[1]['starting_date'], + # ending_date=self.order_meta[1]['ending_date'], + # recurring_period=RecurringPeriod.ONE_TIME, + # price=self.order_meta[1]['price'], + # description=self.order_meta[1]['description'], + # billing_address=BillingAddress.get_address_for(self.user)) -# bills = Bill.create_next_bills_for_user(multi_addr_user) + # bills = Bill.create_next_bills_for_user(multi_addr_user) -# self.assertEqual(len(bills), 2) + # self.assertEqual(len(bills), 2) # # TO BE IMPLEMENTED -- once orders can be marked as "done" / "inactive" / "not for billing" @@ -392,21 +709,21 @@ class BillTestCase(TestCase): # # FIXME: where is the assert? -# class BillingAddressTestCase(TestCase): -# def setUp(self): -# self.user = get_user_model().objects.create( -# username='random_user', -# email='jane.random@domain.tld') +class BillingAddressTestCase(TestCase): + def setUp(self): + self.user = get_user_model().objects.create( + username='random_user', + email='jane.random@domain.tld') -# def test_user_no_address(self): -# """ -# Raise an error, when there is no address -# """ + def test_user_no_address(self): + """ + Raise an error, when there is no address + """ -# self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, -# BillingAddress.get_address_for, -# self.user) + self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist, + BillingAddress.get_address_for, + self.user) # def test_user_only_inactive_address(self): # """ @@ -497,198 +814,6 @@ class BillTestCase(TestCase): -# class ModifyProductTestCase(TestCase): -# def setUp(self): -# self.user = get_user_model().objects.create( -# username='random_user', -# email='jane.random@domain.tld') - -# self.ba = BillingAddress.objects.create( -# owner=self.user, -# organization = 'Test org', -# street="unknown", -# city="unknown", -# postal_code="somewhere else", -# active=True) - -# def test_no_pro_rata_first_bill(self): -# """ -# The bill should NOT contain a partial amount -- this is a BILL TEST :-) -# """ - -# price = 5 - -# # Standard 30d recurring product -# product = SampleRecurringProduct.objects.create(owner=self.user, -# rc_price=price) - -# starting_date = timezone.make_aware(datetime.datetime(2020,3,3)) -# ending_date = timezone.make_aware(datetime.datetime(2020,3,31)) -# time_diff = (ending_date - starting_date).total_seconds() - -# product.create_order(starting_date) - -# bills = Bill.create_next_bills_for_user(self.user, -# ending_date=ending_date) - - -# # We expect 1 bill for 1 billing address and 1 time frame -# self.assertEqual(len(bills), 1) - -# pro_rata_amount = time_diff / product.default_recurring_period.value - -# self.assertNotEqual(bills[0].sum, pro_rata_amount * price) -# self.assertEqual(bills[0].sum, price) - - -# def test_downgrade_product(self): -# """ -# Test downgrading behaviour: - -# We create a recurring product (recurring time: 30 days) and downgrade after 15 days. - -# We create the bill right AFTER the end of the first order. - -# Expected result: - -# - First bill record for 30 days -# - Second bill record starting after 30 days -# - Bill contains two bill records - -# """ - -# user = self.user - - - -# starting_price = 10 -# downgrade_price = 5 - - -# starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) -# first_order_should_end_at = starting_date + datetime.timedelta(days=30) -# change1_date = start_after(starting_date + datetime.timedelta(days=15)) -# bill_ending_date = change1_date + datetime.timedelta(days=1) - - -# product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) -# product.create_order(starting_date) - -# product.rc_price = downgrade_price -# product.save() -# product.create_or_update_recurring_order(when_to_start=change1_date) - -# bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) - -# bill = bills[0] -# bill_records = BillRecord.objects.filter(bill=bill) - -# self.assertEqual(len(bill_records), 2) - -# self.assertEqual(bill_records[0].starting_date, starting_date) - -# self.assertEqual(bill_records[0].order.ending_date, first_order_should_end_at) - -# # self.assertEqual(bill_records[0].ending_date, first_order_should_end_at) - -# # self.assertEqual(bill_records[0].quantity, 1) - -# # self.assertEqual(bill_records[1].quantity, 1) -# # self.assertEqual(int(bill.sum), 15) - -# def test_upgrade_product(self): -# """ -# Test upgrading behaviour -# """ - -# user = self.user - -# # Create product -# starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) -# starting_price = 10 -# product = SampleRecurringProduct.objects.create(owner=user, rc_price=starting_price) -# product.create_order(starting_date) - -# change1_date = start_after(starting_date + datetime.timedelta(days=15)) -# product.rc_price = 20 -# product.save() -# product.create_or_update_recurring_order(when_to_start=change1_date) - -# bill_ending_date = change1_date + datetime.timedelta(days=1) -# bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) - -# bill = bills[0] -# bill_records = BillRecord.objects.filter(bill=bill) - -# self.assertEqual(len(bill_records), 2) -# self.assertEqual(bill_records[0].quantity, .5) - -# self.assertEqual(bill_records[0].ending_date, end_before(change1_date)) - -# self.assertEqual(bill_records[1].quantity, 1) -# self.assertEqual(bill_records[1].starting_date, change1_date) - -# self.assertEqual(int(bill.sum), 25) - - - - # def test_bill_for_increasing_product(self): - # """ - # Modify a product, see one pro rata entry - # """ - - # # Create product - # starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) - # starting_price = 30.5 - # product = SampleRecurringProduct.objects.create(owner=self.user, - # rc_price=starting_price) - # product.create_order(starting_date) - - # recurring_period = product.default_recurring_period.value - - # # First change - # change1_date = timezone.make_aware(datetime.datetime(2019,4,17)) - # product.rc_price = 49.5 - # product.save() - # product.create_or_update_recurring_order(when_to_start=change1_date) - - # # Second change - # change2_date = timezone.make_aware(datetime.datetime(2019,5,8)) - # product.rc_price = 56.5 - # product.save() - # product.create_or_update_recurring_order(when_to_start=change2_date) - - # # Create bill one month after 2nd change - # bill_ending_date = timezone.make_aware(datetime.datetime(2019,6,30)) - # bills = Bill.create_next_bills_for_user(self.user, - # ending_date=bill_ending_date) - - # # only one bill in this test case - # bill = bills[0] - - # expected_amount = starting_price - - # d2 = starting_date + recurring_period - # duration2 = change1_date - d2 - - # expected_amount = 0 - - # # Expected bill sum & records: - # # 2019-03-03 - 2019-04-02 +30d: 30.5 - # # 2019-04-02 - 2019-04-17: +15d: 15.25 - # # 2019-04-17 - 2019-05-08: +21d: (21/30) * 49.5 - # # 2019-05-08 - 2019-06-07: +30d: 56.5 - # # 2019-06-07 - 2019-07-07: +30d: 56.5 - - - # self.assertEqual(bills[0].sum, price) - - # # expeted result: - # # 1x 5 chf bill record - # # 1x 5 chf bill record - # # 1x 10 partial bill record - - # class NotABillingTC(TestCase): # #class BillingTestCase(TestCase): diff --git a/uncloud_service/models.py b/uncloud_service/models.py index 8afb2e7..a37e42b 100644 --- a/uncloud_service/models.py +++ b/uncloud_service/models.py @@ -15,8 +15,8 @@ class MatrixServiceProduct(models.Model): domain = models.CharField(max_length=255, default='domain.tld') # Default recurring price is PER_MONT, see Product class. - def recurring_price(self, recurring_period=RecurringPeriod.PER_30D): - return self.monthly_managment_fee + # def recurring_price(self, recurring_period=RecurringPeriod.PER_30D): + # return self.monthly_managment_fee @staticmethod def base_image(): @@ -24,11 +24,11 @@ class MatrixServiceProduct(models.Model): #e return VMDiskImageProduct.objects.get(uuid="93e564c5-adb3-4741-941f-718f76075f02") return False - @staticmethod - def allowed_recurring_periods(): - return list(filter( - lambda pair: pair[0] in [RecurringPeriod.PER_30D], - RecurringPeriod.choices)) + # @staticmethod + # def allowed_recurring_periods(): + # return list(filter( + # lambda pair: pair[0] in [RecurringPeriod.PER_30D], + # RecurringPeriod.choices)) @property def one_time_price(self): diff --git a/uncloud_service/serializers.py b/uncloud_service/serializers.py index 8dbd547..bc6d753 100644 --- a/uncloud_service/serializers.py +++ b/uncloud_service/serializers.py @@ -17,8 +17,8 @@ class MatrixServiceProductSerializer(serializers.ModelSerializer): read_only_fields = ['order', 'owner', 'status'] class OrderMatrixServiceProductSerializer(MatrixServiceProductSerializer): - recurring_period = serializers.ChoiceField( - choices=MatrixServiceProduct.allowed_recurring_periods()) + # recurring_period = serializers.ChoiceField( + # choices=MatrixServiceProduct.allowed_recurring_periods()) def __init__(self, *args, **kwargs): super(OrderMatrixServiceProductSerializer, self).__init__(*args, **kwargs) @@ -42,8 +42,8 @@ class GenericServiceProductSerializer(serializers.ModelSerializer): read_only_fields = [ 'owner', 'status'] class OrderGenericServiceProductSerializer(GenericServiceProductSerializer): - recurring_period = serializers.ChoiceField( - choices=GenericServiceProduct.allowed_recurring_periods()) + # recurring_period = serializers.ChoiceField( + # choices=GenericServiceProduct.allowed_recurring_periods()) def __init__(self, *args, **kwargs): super(OrderGenericServiceProductSerializer, self).__init__(*args, **kwargs) diff --git a/uncloud_vm/models.py b/uncloud_vm/models.py index 72a9555..c605779 100644 --- a/uncloud_vm/models.py +++ b/uncloud_vm/models.py @@ -71,12 +71,12 @@ class VMProduct(models.Model): return "Virtual machine '{}': {} core(s), {}GB memory".format( self.name, self.cores, self.ram_in_gb) - @staticmethod - def allowed_recurring_periods(): - return list(filter( - lambda pair: pair[0] in [RecurringPeriod.PER_365D, - RecurringPeriod.PER_30D, RecurringPeriod.PER_HOUR], - RecurringPeriod.choices)) + # @staticmethod + # def allowed_recurring_periods(): + # return list(filter( + # lambda pair: pair[0] in [RecurringPeriod.PER_365D, + # RecurringPeriod.PER_30D, RecurringPeriod.PER_HOUR], + # RecurringPeriod.choices)) def __str__(self): diff --git a/uncloud_vm/serializers.py b/uncloud_vm/serializers.py index 5032ad4..a60d10b 100644 --- a/uncloud_vm/serializers.py +++ b/uncloud_vm/serializers.py @@ -101,8 +101,8 @@ class VMProductSerializer(serializers.ModelSerializer): read_only_fields = ['order', 'owner', 'status'] class OrderVMProductSerializer(VMProductSerializer): - recurring_period = serializers.ChoiceField( - choices=VMWithOSProduct.allowed_recurring_periods()) + # recurring_period = serializers.ChoiceField( + # choices=VMWithOSProduct.allowed_recurring_periods()) def __init__(self, *args, **kwargs): super(VMProductSerializer, self).__init__(*args, **kwargs) @@ -133,8 +133,8 @@ class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer): """ # Custom field used at creation (= ordering) only. - recurring_period = serializers.ChoiceField( - choices=VMProduct.allowed_recurring_periods()) + # recurring_period = serializers.ChoiceField( + # choices=VMProduct.allowed_recurring_periods()) os_disk_uuid = serializers.UUIDField() # os_disk_size =