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:
Nico Schottelius 2020-10-06 15:46:22 +02:00
commit 992c7c551e
11 changed files with 588 additions and 362 deletions

View file

@ -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