Updating for products/recurring periods

This commit is contained in:
Nico Schottelius 2020-10-06 18:53:13 +02:00
commit 9623a77907
7 changed files with 232 additions and 58 deletions

View file

@ -252,8 +252,29 @@ class RecurringPeriod(models.Model):
defaults={ 'duration_seconds': seconds })
@staticmethod
def secs_to_name(secs):
name = ""
days = 0
hours = 0
if secs > 24*3600:
days = secs // (24*3600)
secs -= (days*24*3600)
if secs > 3600:
hours = secs // 3600
secs -= hours*3600
return f"{days} days {hours} hours {secs} seconds"
def __str__(self):
return f"{self.name} ({self.duration_seconds})"
duration = self.secs_to_name(self.duration_seconds)
return f"{self.name} ({duration})"
###
@ -329,17 +350,64 @@ class Product(UncloudModel):
"""
name = models.CharField(max_length=256)
name = models.CharField(max_length=256, unique=True)
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.ForeignKey(RecurringPeriod, on_delete=models.CASCADE, editable=True)
recurring_periods = models.ManyToManyField(RecurringPeriod, through='ProductToRecurringPeriod')
currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF)
@property
def default_recurring_period(self):
return RecurringPeriod.objects.get(product=self,
is_default=True)
@classmethod
def populate_db_defaults(cls):
recurring_period = RecurringPeriod.objects.get(name="Per 30 days")
obj, created = cls.objects.get_or_create(name="Dual Stack Virtual Machine v1",
description="A standard virtual machine",
currency=Currency.CHF,
config={
'features': {
'base':
{ 'min': 1,
'max': 1,
'one_time_price_per_unit': 0,
'recurring_price_per_unit': 8
},
'cores':
{ 'min': 1,
'max': 48,
'one_time_price_per_unit': 0,
'recurring_price_per_unit': 3
},
'ram_gb':
{ 'min': 1,
'max': 256,
'one_time_price_per_unit': 0,
'recurring_price_per_unit': 4
},
'ssd_gb':
{ 'min': 1,
'one_time_price_per_unit': 0,
'recurring_price_per_unit': 3.5
},
'hdd_gb':
{ 'min': 0,
'one_time_price_per_unit': 0,
'recurring_price_per_unit': 15/1000
},
}
}
)
obj.recurring_periods.add(recurring_period, through_defaults= { 'is_default': True })
def __str__(self):
return f"{self.name} - {self.description}"
@property
def recurring_orders(self):
return self.orders.order_by('id').exclude(recurring_period=RecurringPeriod.ONE_TIME)
@ -432,18 +500,6 @@ class Product(UncloudModel):
else:
self.create_order(when_to_start, recurring_period)
@property
def recurring_price(self):
""" implement correct values in the child class """
return 0
@property
def one_time_price(self):
""" implement correct values in the child class """
return 0
@property
def is_recurring(self):
return self.recurring_price > 0
@ -452,13 +508,6 @@ class Product(UncloudModel):
def billing_address(self):
return self.order.billing_address
@staticmethod
def allowed_recurring_periods():
return RecurringPeriod.choices
# class Meta:
# abstract = True
def discounted_price_by_period(self, requested_period):
"""
Each product has a standard recurring period for which
@ -553,7 +602,6 @@ class Order(models.Model):
Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating
bills. Do **NOT** mutate then!
An one time order is "closed" (does not need to be billed anymore)
if it has one bill record. Having more than one is a programming
error.
@ -586,10 +634,9 @@ class Order(models.Model):
starting_date = models.DateTimeField(default=timezone.now)
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.ForeignKey(RecurringPeriod, on_delete=models.CASCADE, editable=True)
recurring_period = models.ForeignKey(RecurringPeriod,
on_delete=models.CASCADE,
editable=True)
one_time_price = models.DecimalField(default=0.0,
max_digits=AMOUNT_MAX_DIGITS,
@ -823,24 +870,33 @@ class Order(models.Model):
ending_date=ending_date,
is_recurring_record=True)
@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']:
# FIXME: support optional features (?)
if not feature in self.config['features']:
raise ValidationError(f"Configuration is missing feature {feature}")
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]
# Set min to 0 if not specified
min_val = self.product.config['features'][feature].get('min', 0)
# We might not even have 'features' cannot use .get() on it
try:
value = self.config['features'][feature]
except KeyError:
value = self.product.config['features'][feature]['min']
# Set max to current value if not specified
max_val = self.product.config['features'][feature].get('max', value)
if value < min_val or value > max_val:
raise ValidationError(f"Feature '{feature}' must be at least {min_val} and at maximum {max_val}. Value is: {value}")
one_time_price += self.product.config['features'][feature]['one_time_price_per_unit'] * value
recurring_price += self.product.config['features'][feature]['recurring_price_per_unit'] * value
return (one_time_price, recurring_price)
@ -851,6 +907,11 @@ class Order(models.Model):
if self._state.adding:
(self.one_time_price, self.recurring_price) = self.prices
if not self.recurring_period:
self.recurring_period = self.product.default_recurring_period
# FIXME: ensure the recurring period is defined in the product
super().save(*args, **kwargs)
@ -1187,6 +1248,28 @@ class BillRecord(models.Model):
super().save(*args, **kwargs)
class ProductToRecurringPeriod(models.Model):
"""
Intermediate manytomany mapping class
"""
recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE)
product = models.ForeignKey(Product, on_delete=models.CASCADE)
is_default = models.BooleanField(default=False)
class Meta:
constraints = [
models.UniqueConstraint(fields=['product'],
condition=Q(is_default=True),
name='one_default_recurring_period_per_product')
]
def __str__(self):
return f"{self.product} - {self.recurring_period} (default: {self.is_default})"
# # Sample products included into uncloud
# class SampleOneTimeProduct(models.Model):
# """