forked from uncloud/uncloud
Make recurring period a database model
- For easier handling (foreignkeys, many2many) - For higher flexibility (users can define their own periods)
This commit is contained in:
parent
58883765d7
commit
992c7c551e
11 changed files with 588 additions and 362 deletions
11
uncloud/management/commands/db-add-defaults.py
Normal file
11
uncloud/management/commands/db-add-defaults.py
Normal file
|
@ -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()
|
|
@ -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):
|
||||
|
|
|
@ -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 ]:
|
||||
|
|
41
uncloud_pay/migrations/0027_auto_20201006_1319.py
Normal file
41
uncloud_pay/migrations/0027_auto_20201006_1319.py
Normal file
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,30 +518,33 @@ 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()
|
||||
|
||||
self.assertEqual(len(self.bill_dates), bill_count)
|
||||
|
||||
# def test_multi_addr_multi_bill(self):
|
||||
# """
|
||||
|
@ -256,6 +564,15 @@ class BillTestCase(TestCase):
|
|||
# postal_code="unknown",
|
||||
# active=True)
|
||||
|
||||
# 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)
|
||||
|
||||
# order1 = Order.objects.create(
|
||||
# owner=multi_addr_user,
|
||||
# starting_date=self.order_meta[1]['starting_date'],
|
||||
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 =
|
||||
|
|
Loading…
Reference in a new issue