Add JSON support for product description
This commit is contained in:
parent
c6bacab35a
commit
c32499199a
10 changed files with 1589 additions and 653 deletions
|
@ -11,7 +11,7 @@ from django.http import FileResponse
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
|
||||||
from uncloud_pay.models import Bill, Order, BillRecord, BillingAddress, SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee
|
from uncloud_pay.models import Bill, Order, BillRecord, BillingAddress, Product
|
||||||
|
|
||||||
|
|
||||||
class BillRecordInline(admin.TabularInline):
|
class BillRecordInline(admin.TabularInline):
|
||||||
|
@ -91,7 +91,8 @@ admin.site.register(Order)
|
||||||
admin.site.register(BillRecord)
|
admin.site.register(BillRecord)
|
||||||
admin.site.register(BillingAddress)
|
admin.site.register(BillingAddress)
|
||||||
|
|
||||||
for m in [ SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee ]:
|
#for m in [ SampleOneTimeProduct, SampleRecurringProduct, SampleRecurringProductOneTimeFee ]:
|
||||||
admin.site.register(m)
|
|
||||||
|
admin.site.register(Product)
|
||||||
|
|
||||||
#admin.site.register(Order, OrderAdmin)
|
#admin.site.register(Order, OrderAdmin)
|
||||||
|
|
19
uncloud_pay/migrations/0017_order_config.py
Normal file
19
uncloud_pay/migrations/0017_order_config.py
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 3.1 on 2020-09-28 19:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_pay', '0016_auto_20200928_1858'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='config',
|
||||||
|
field=models.JSONField(default={}),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
20
uncloud_pay/migrations/0018_order_product.py
Normal file
20
uncloud_pay/migrations/0018_order_product.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.1 on 2020-09-28 19:08
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_pay', '0017_order_config'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='product',
|
||||||
|
field=models.ForeignKey(default=0, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.product'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
17
uncloud_pay/migrations/0019_remove_product_owner.py
Normal file
17
uncloud_pay/migrations/0019_remove_product_owner.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.1 on 2020-09-28 19:14
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_pay', '0018_order_product'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='owner',
|
||||||
|
),
|
||||||
|
]
|
29
uncloud_pay/migrations/0020_auto_20200928_1915.py
Normal file
29
uncloud_pay/migrations/0020_auto_20200928_1915.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
# Generated by Django 3.1 on 2020-09-28 19:15
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_pay', '0019_remove_product_owner'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='product',
|
||||||
|
name='status',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='description',
|
||||||
|
field=models.CharField(default='', max_length=1024),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(default='', max_length=256),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
23
uncloud_pay/migrations/0021_auto_20200928_1932.py
Normal file
23
uncloud_pay/migrations/0021_auto_20200928_1932.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 3.1 on 2020-09-28 19:32
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_pay', '0020_auto_20200928_1915'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='default_currency',
|
||||||
|
field=models.CharField(choices=[('CHF', 'Swiss Franc'), ('EUR', 'Euro'), ('USD', 'US Dollar')], default='CHF', max_length=32),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='default_recurring_period',
|
||||||
|
field=models.IntegerField(choices=[(31536000, 'Per 365 days'), (2592000, 'Per 30 days'), (604800, 'Per Week'), (86400, 'Per Day'), (3600, 'Per Hour'), (60, 'Per Minute'), (1, 'Per Second'), (0, 'Onetime')], default=2592000),
|
||||||
|
),
|
||||||
|
]
|
18
uncloud_pay/migrations/0022_auto_20200928_1932.py
Normal file
18
uncloud_pay/migrations/0022_auto_20200928_1932.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.1 on 2020-09-28 19:32
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('uncloud_pay', '0021_auto_20200928_1932'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='product',
|
||||||
|
old_name='default_currency',
|
||||||
|
new_name='currency',
|
||||||
|
),
|
||||||
|
]
|
|
@ -80,6 +80,16 @@ class RecurringPeriod(models.IntegerChoices):
|
||||||
PER_SECOND = 1, _('Per Second')
|
PER_SECOND = 1, _('Per Second')
|
||||||
ONE_TIME = 0, _('Onetime')
|
ONE_TIME = 0, _('Onetime')
|
||||||
|
|
||||||
|
class Currency(models.TextChoices):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
CHF = 'CHF', _('Swiss Franc')
|
||||||
|
EUR = 'EUR', _('Euro')
|
||||||
|
USD = 'USD', _('US Dollar')
|
||||||
|
|
||||||
class CountryField(models.CharField):
|
class CountryField(models.CharField):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs.setdefault('choices', COUNTRIES)
|
kwargs.setdefault('choices', COUNTRIES)
|
||||||
|
@ -283,6 +293,235 @@ class VATRate(models.Model):
|
||||||
logger.debug("Did not find VAT rate for %s, returning 0" % country_code)
|
logger.debug("Did not find VAT rate for %s, returning 0" % country_code)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
###
|
||||||
|
# Products
|
||||||
|
|
||||||
|
class Product(UncloudModel):
|
||||||
|
"""
|
||||||
|
A product is something a user can order. To record the pricing, we
|
||||||
|
create order that define a state in time.
|
||||||
|
|
||||||
|
A product can have *one* one_time_order and/or *one*
|
||||||
|
recurring_order.
|
||||||
|
|
||||||
|
If either of them needs to be updated, a new order of the same
|
||||||
|
type will be created and links to the previous order.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = models.CharField(max_length=256)
|
||||||
|
description = models.CharField(max_length=1024)
|
||||||
|
|
||||||
|
config = models.JSONField()
|
||||||
|
default_recurring_period = models.IntegerField(choices=RecurringPeriod.choices, default=RecurringPeriod.PER_30D)
|
||||||
|
currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recurring_orders(self):
|
||||||
|
return self.orders.order_by('id').exclude(recurring_period=RecurringPeriod.ONE_TIME)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_recurring_order(self):
|
||||||
|
return self.recurring_orders.last()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def one_time_orders(self):
|
||||||
|
return self.orders.order_by('id').filter(recurring_period=RecurringPeriod.ONE_TIME)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_one_time_order(self):
|
||||||
|
return self.one_time_orders.last()
|
||||||
|
|
||||||
|
def create_order(self, when_to_start=None, recurring_period=None):
|
||||||
|
billing_address = BillingAddress.get_address_for(self.owner)
|
||||||
|
|
||||||
|
if not billing_address:
|
||||||
|
raise ValidationError("Cannot order without a billing address")
|
||||||
|
|
||||||
|
if not when_to_start:
|
||||||
|
when_to_start = timezone.now()
|
||||||
|
|
||||||
|
if not recurring_period:
|
||||||
|
recurring_period = self.default_recurring_period
|
||||||
|
|
||||||
|
|
||||||
|
# Create one time order if we did not create one already
|
||||||
|
if self.one_time_price > 0 and not self.last_one_time_order:
|
||||||
|
one_time_order = Order.objects.create(owner=self.owner,
|
||||||
|
billing_address=billing_address,
|
||||||
|
starting_date=when_to_start,
|
||||||
|
price=self.one_time_price,
|
||||||
|
recurring_period=RecurringPeriod.ONE_TIME,
|
||||||
|
description=str(self))
|
||||||
|
self.orders.add(one_time_order)
|
||||||
|
else:
|
||||||
|
one_time_order = None
|
||||||
|
|
||||||
|
if recurring_period != RecurringPeriod.ONE_TIME:
|
||||||
|
if one_time_order:
|
||||||
|
recurring_order = Order.objects.create(owner=self.owner,
|
||||||
|
billing_address=billing_address,
|
||||||
|
starting_date=when_to_start,
|
||||||
|
price=self.recurring_price,
|
||||||
|
recurring_period=recurring_period,
|
||||||
|
depends_on=one_time_order,
|
||||||
|
description=str(self))
|
||||||
|
else:
|
||||||
|
recurring_order = Order.objects.create(owner=self.owner,
|
||||||
|
billing_address=billing_address,
|
||||||
|
starting_date=when_to_start,
|
||||||
|
price=self.recurring_price,
|
||||||
|
recurring_period=recurring_period,
|
||||||
|
description=str(self))
|
||||||
|
self.orders.add(recurring_order)
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: this could/should be part of Order (?)
|
||||||
|
def create_or_update_recurring_order(self, when_to_start=None, recurring_period=None):
|
||||||
|
if not self.recurring_price:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not recurring_period:
|
||||||
|
recurring_period = self.default_recurring_period
|
||||||
|
|
||||||
|
if not when_to_start:
|
||||||
|
when_to_start = timezone.now()
|
||||||
|
|
||||||
|
if self.last_recurring_order:
|
||||||
|
if self.recurring_price < self.last_recurring_order.price:
|
||||||
|
|
||||||
|
if when_to_start < self.last_recurring_order.next_cancel_or_downgrade_date:
|
||||||
|
when_to_start = start_after(self.last_recurring_order.next_cancel_or_downgrade_date)
|
||||||
|
|
||||||
|
when_to_end = end_before(when_to_start)
|
||||||
|
|
||||||
|
new_order = Order.objects.create(owner=self.owner,
|
||||||
|
billing_address=self.last_recurring_order.billing_address,
|
||||||
|
starting_date=when_to_start,
|
||||||
|
price=self.recurring_price,
|
||||||
|
recurring_period=recurring_period,
|
||||||
|
description=str(self),
|
||||||
|
replaces=self.last_recurring_order)
|
||||||
|
|
||||||
|
self.last_recurring_order.replace_with(new_order)
|
||||||
|
self.orders.add(new_order)
|
||||||
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
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
|
||||||
|
we define a pricing. I.e. VPN is usually year, VM is usually monthly.
|
||||||
|
|
||||||
|
The user can opt-in to use a different period, which influences the price:
|
||||||
|
The longer a user commits, the higher the discount.
|
||||||
|
|
||||||
|
Products can also be limited in the available periods. For instance
|
||||||
|
a VPN only makes sense to be bought for at least one day.
|
||||||
|
|
||||||
|
Rules are as follows:
|
||||||
|
|
||||||
|
given a standard recurring period of ..., changing to ... modifies price ...
|
||||||
|
|
||||||
|
|
||||||
|
# One month for free if buying / year, compared to a month: about 8.33% discount
|
||||||
|
per_year -> per_month -> /11
|
||||||
|
per_month -> per_year -> *11
|
||||||
|
|
||||||
|
# Month has 30.42 days on average. About 7.9% discount to go monthly
|
||||||
|
per_month -> per_day -> /28
|
||||||
|
per_day -> per_month -> *28
|
||||||
|
|
||||||
|
# Day has 24h, give one for free
|
||||||
|
per_day -> per_hour -> /23
|
||||||
|
per_hour -> per_day -> /23
|
||||||
|
|
||||||
|
|
||||||
|
Examples
|
||||||
|
|
||||||
|
VPN @ 120CHF/y becomes
|
||||||
|
- 10.91 CHF/month (130.91 CHF/year)
|
||||||
|
- 0.39 CHF/day (142.21 CHF/year)
|
||||||
|
|
||||||
|
VM @ 15 CHF/month becomes
|
||||||
|
- 165 CHF/month (13.75 CHF/month)
|
||||||
|
- 0.54 CHF/day (16.30 CHF/month)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
if self.default_recurring_period == RecurringPeriod.PER_365D:
|
||||||
|
if requested_period == RecurringPeriod.PER_365D:
|
||||||
|
return self.recurring_price
|
||||||
|
if requested_period == RecurringPeriod.PER_30D:
|
||||||
|
return self.recurring_price/11.
|
||||||
|
if requested_period == RecurringPeriod.PER_DAY:
|
||||||
|
return self.recurring_price/11./28.
|
||||||
|
|
||||||
|
elif self.default_recurring_period == RecurringPeriod.PER_30D:
|
||||||
|
if requested_period == RecurringPeriod.PER_365D:
|
||||||
|
return self.recurring_price*11
|
||||||
|
if requested_period == RecurringPeriod.PER_30D:
|
||||||
|
return self.recurring_price
|
||||||
|
if requested_period == RecurringPeriod.PER_DAY:
|
||||||
|
return self.recurring_price/28.
|
||||||
|
|
||||||
|
elif self.default_recurring_period == RecurringPeriod.PER_DAY:
|
||||||
|
if requested_period == RecurringPeriod.PER_365D:
|
||||||
|
return self.recurring_price*11*28
|
||||||
|
if requested_period == RecurringPeriod.PER_30D:
|
||||||
|
return self.recurring_price*28
|
||||||
|
if requested_period == RecurringPeriod.PER_DAY:
|
||||||
|
return self.recurring_price
|
||||||
|
else:
|
||||||
|
# FIXME: use the right type of exception here!
|
||||||
|
raise Exception("Did not implement the discounter for this case")
|
||||||
|
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# try:
|
||||||
|
# ba = BillingAddress.get_address_for(self.owner)
|
||||||
|
# except BillingAddress.DoesNotExist:
|
||||||
|
# raise ValidationError("User does not have a billing address")
|
||||||
|
|
||||||
|
# if not ba.active:
|
||||||
|
# raise ValidationError("User does not have an active billing address")
|
||||||
|
|
||||||
|
|
||||||
|
# Verify the required JSON fields
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###
|
###
|
||||||
# Orders.
|
# Orders.
|
||||||
|
|
||||||
|
@ -317,7 +556,9 @@ class Order(models.Model):
|
||||||
|
|
||||||
description = models.TextField()
|
description = models.TextField()
|
||||||
|
|
||||||
# TODO: enforce ending_date - starting_date to be larger than recurring_period.
|
product = models.ForeignKey(Product, blank=False, null=False, on_delete=models.CASCADE)
|
||||||
|
config = models.JSONField()
|
||||||
|
|
||||||
creation_date = models.DateTimeField(auto_now_add=True)
|
creation_date = models.DateTimeField(auto_now_add=True)
|
||||||
starting_date = models.DateTimeField(default=timezone.now)
|
starting_date = models.DateTimeField(default=timezone.now)
|
||||||
ending_date = models.DateTimeField(blank=True, null=True)
|
ending_date = models.DateTimeField(blank=True, null=True)
|
||||||
|
@ -850,237 +1091,6 @@ class BillRecord(models.Model):
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
###
|
|
||||||
# Products
|
|
||||||
|
|
||||||
class Product(UncloudModel):
|
|
||||||
"""
|
|
||||||
A product is something a user can order. To record the pricing, we
|
|
||||||
create order that define a state in time.
|
|
||||||
|
|
||||||
A product can have *one* one_time_order and/or *one*
|
|
||||||
recurring_order.
|
|
||||||
|
|
||||||
If either of them needs to be updated, a new order of the same
|
|
||||||
type will be created and links to the previous order.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
owner = models.ForeignKey(get_user_model(),
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
editable=False)
|
|
||||||
|
|
||||||
description = "Generic Product"
|
|
||||||
|
|
||||||
status = models.CharField(max_length=32,
|
|
||||||
choices=UncloudStatus.choices,
|
|
||||||
default=UncloudStatus.AWAITING_PAYMENT)
|
|
||||||
|
|
||||||
config = models.JSONField()
|
|
||||||
|
|
||||||
# Default period for all products
|
|
||||||
default_recurring_period = RecurringPeriod.PER_30D
|
|
||||||
|
|
||||||
@property
|
|
||||||
def recurring_orders(self):
|
|
||||||
return self.orders.order_by('id').exclude(recurring_period=RecurringPeriod.ONE_TIME)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_recurring_order(self):
|
|
||||||
return self.recurring_orders.last()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def one_time_orders(self):
|
|
||||||
return self.orders.order_by('id').filter(recurring_period=RecurringPeriod.ONE_TIME)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_one_time_order(self):
|
|
||||||
return self.one_time_orders.last()
|
|
||||||
|
|
||||||
def create_order(self, when_to_start=None, recurring_period=None):
|
|
||||||
billing_address = BillingAddress.get_address_for(self.owner)
|
|
||||||
|
|
||||||
if not billing_address:
|
|
||||||
raise ValidationError("Cannot order without a billing address")
|
|
||||||
|
|
||||||
if not when_to_start:
|
|
||||||
when_to_start = timezone.now()
|
|
||||||
|
|
||||||
if not recurring_period:
|
|
||||||
recurring_period = self.default_recurring_period
|
|
||||||
|
|
||||||
|
|
||||||
# Create one time order if we did not create one already
|
|
||||||
if self.one_time_price > 0 and not self.last_one_time_order:
|
|
||||||
one_time_order = Order.objects.create(owner=self.owner,
|
|
||||||
billing_address=billing_address,
|
|
||||||
starting_date=when_to_start,
|
|
||||||
price=self.one_time_price,
|
|
||||||
recurring_period=RecurringPeriod.ONE_TIME,
|
|
||||||
description=str(self))
|
|
||||||
self.orders.add(one_time_order)
|
|
||||||
else:
|
|
||||||
one_time_order = None
|
|
||||||
|
|
||||||
if recurring_period != RecurringPeriod.ONE_TIME:
|
|
||||||
if one_time_order:
|
|
||||||
recurring_order = Order.objects.create(owner=self.owner,
|
|
||||||
billing_address=billing_address,
|
|
||||||
starting_date=when_to_start,
|
|
||||||
price=self.recurring_price,
|
|
||||||
recurring_period=recurring_period,
|
|
||||||
depends_on=one_time_order,
|
|
||||||
description=str(self))
|
|
||||||
else:
|
|
||||||
recurring_order = Order.objects.create(owner=self.owner,
|
|
||||||
billing_address=billing_address,
|
|
||||||
starting_date=when_to_start,
|
|
||||||
price=self.recurring_price,
|
|
||||||
recurring_period=recurring_period,
|
|
||||||
description=str(self))
|
|
||||||
self.orders.add(recurring_order)
|
|
||||||
|
|
||||||
|
|
||||||
# FIXME: this could/should be part of Order (?)
|
|
||||||
def create_or_update_recurring_order(self, when_to_start=None, recurring_period=None):
|
|
||||||
if not self.recurring_price:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not recurring_period:
|
|
||||||
recurring_period = self.default_recurring_period
|
|
||||||
|
|
||||||
if not when_to_start:
|
|
||||||
when_to_start = timezone.now()
|
|
||||||
|
|
||||||
if self.last_recurring_order:
|
|
||||||
if self.recurring_price < self.last_recurring_order.price:
|
|
||||||
|
|
||||||
if when_to_start < self.last_recurring_order.next_cancel_or_downgrade_date:
|
|
||||||
when_to_start = start_after(self.last_recurring_order.next_cancel_or_downgrade_date)
|
|
||||||
|
|
||||||
when_to_end = end_before(when_to_start)
|
|
||||||
|
|
||||||
new_order = Order.objects.create(owner=self.owner,
|
|
||||||
billing_address=self.last_recurring_order.billing_address,
|
|
||||||
starting_date=when_to_start,
|
|
||||||
price=self.recurring_price,
|
|
||||||
recurring_period=recurring_period,
|
|
||||||
description=str(self),
|
|
||||||
replaces=self.last_recurring_order)
|
|
||||||
|
|
||||||
self.last_recurring_order.replace_with(new_order)
|
|
||||||
self.orders.add(new_order)
|
|
||||||
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
|
|
||||||
|
|
||||||
@property
|
|
||||||
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
|
|
||||||
we define a pricing. I.e. VPN is usually year, VM is usually monthly.
|
|
||||||
|
|
||||||
The user can opt-in to use a different period, which influences the price:
|
|
||||||
The longer a user commits, the higher the discount.
|
|
||||||
|
|
||||||
Products can also be limited in the available periods. For instance
|
|
||||||
a VPN only makes sense to be bought for at least one day.
|
|
||||||
|
|
||||||
Rules are as follows:
|
|
||||||
|
|
||||||
given a standard recurring period of ..., changing to ... modifies price ...
|
|
||||||
|
|
||||||
|
|
||||||
# One month for free if buying / year, compared to a month: about 8.33% discount
|
|
||||||
per_year -> per_month -> /11
|
|
||||||
per_month -> per_year -> *11
|
|
||||||
|
|
||||||
# Month has 30.42 days on average. About 7.9% discount to go monthly
|
|
||||||
per_month -> per_day -> /28
|
|
||||||
per_day -> per_month -> *28
|
|
||||||
|
|
||||||
# Day has 24h, give one for free
|
|
||||||
per_day -> per_hour -> /23
|
|
||||||
per_hour -> per_day -> /23
|
|
||||||
|
|
||||||
|
|
||||||
Examples
|
|
||||||
|
|
||||||
VPN @ 120CHF/y becomes
|
|
||||||
- 10.91 CHF/month (130.91 CHF/year)
|
|
||||||
- 0.39 CHF/day (142.21 CHF/year)
|
|
||||||
|
|
||||||
VM @ 15 CHF/month becomes
|
|
||||||
- 165 CHF/month (13.75 CHF/month)
|
|
||||||
- 0.54 CHF/day (16.30 CHF/month)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
if self.default_recurring_period == RecurringPeriod.PER_365D:
|
|
||||||
if requested_period == RecurringPeriod.PER_365D:
|
|
||||||
return self.recurring_price
|
|
||||||
if requested_period == RecurringPeriod.PER_30D:
|
|
||||||
return self.recurring_price/11.
|
|
||||||
if requested_period == RecurringPeriod.PER_DAY:
|
|
||||||
return self.recurring_price/11./28.
|
|
||||||
|
|
||||||
elif self.default_recurring_period == RecurringPeriod.PER_30D:
|
|
||||||
if requested_period == RecurringPeriod.PER_365D:
|
|
||||||
return self.recurring_price*11
|
|
||||||
if requested_period == RecurringPeriod.PER_30D:
|
|
||||||
return self.recurring_price
|
|
||||||
if requested_period == RecurringPeriod.PER_DAY:
|
|
||||||
return self.recurring_price/28.
|
|
||||||
|
|
||||||
elif self.default_recurring_period == RecurringPeriod.PER_DAY:
|
|
||||||
if requested_period == RecurringPeriod.PER_365D:
|
|
||||||
return self.recurring_price*11*28
|
|
||||||
if requested_period == RecurringPeriod.PER_30D:
|
|
||||||
return self.recurring_price*28
|
|
||||||
if requested_period == RecurringPeriod.PER_DAY:
|
|
||||||
return self.recurring_price
|
|
||||||
else:
|
|
||||||
# FIXME: use the right type of exception here!
|
|
||||||
raise Exception("Did not implement the discounter for this case")
|
|
||||||
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
ba = BillingAddress.get_address_for(self.owner)
|
|
||||||
except BillingAddress.DoesNotExist:
|
|
||||||
raise ValidationError("User does not have a billing address")
|
|
||||||
|
|
||||||
if not ba.active:
|
|
||||||
raise ValidationError("User does not have an active billing address")
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# Sample products included into uncloud
|
# Sample products included into uncloud
|
||||||
class SampleOneTimeProduct(models.Model):
|
class SampleOneTimeProduct(models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
721
uncloud_pay/tests-before-refactor.py
Normal file
721
uncloud_pay/tests-before-refactor.py
Normal file
|
@ -0,0 +1,721 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from datetime import datetime, date, timedelta
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from .models import *
|
||||||
|
from uncloud_service.models import GenericServiceProduct
|
||||||
|
|
||||||
|
class OrderTestCase(TestCase):
|
||||||
|
"""
|
||||||
|
The heart of ordering products
|
||||||
|
"""
|
||||||
|
|
||||||
|
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_create_one_time_product(self):
|
||||||
|
"""
|
||||||
|
One time payment products cannot be updated - can they?
|
||||||
|
"""
|
||||||
|
|
||||||
|
p = SampleOneTimeProduct.objects.create(owner=self.user)
|
||||||
|
|
||||||
|
self.assertEqual(p.one_time_price, 5)
|
||||||
|
self.assertEqual(p.recurring_price, 0)
|
||||||
|
|
||||||
|
|
||||||
|
# class ProductTestCase(TestCase):
|
||||||
|
# """
|
||||||
|
# Test products and products <-> order interaction
|
||||||
|
# """
|
||||||
|
|
||||||
|
# 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_create_one_time_product(self):
|
||||||
|
# """
|
||||||
|
# One time payment products cannot be updated - can they?
|
||||||
|
# """
|
||||||
|
|
||||||
|
# p = SampleOneTimeProduct.objects.create(owner=self.user)
|
||||||
|
|
||||||
|
# self.assertEqual(p.one_time_price, 5)
|
||||||
|
# self.assertEqual(p.recurring_price, 0)
|
||||||
|
|
||||||
|
# def test_create_product_without_active_billing_address(self):
|
||||||
|
# """
|
||||||
|
# Fail to create a product without an active billing address
|
||||||
|
# """
|
||||||
|
|
||||||
|
# self.ba.active = False
|
||||||
|
# self.ba.save()
|
||||||
|
|
||||||
|
# with self.assertRaises(ValidationError):
|
||||||
|
# p = SampleOneTimeProduct.objects.create(owner=self.user)
|
||||||
|
|
||||||
|
# def test_create_product_without_billing_address(self):
|
||||||
|
# """
|
||||||
|
# Fail to create a product without a billing address
|
||||||
|
# """
|
||||||
|
|
||||||
|
# user2 = get_user_model().objects.create(
|
||||||
|
# username='random_user2',
|
||||||
|
# email='jane.randomly@domain.tld')
|
||||||
|
|
||||||
|
# with self.assertRaises(ValidationError):
|
||||||
|
# p = SampleOneTimeProduct.objects.create(owner=user2)
|
||||||
|
|
||||||
|
|
||||||
|
# def test_create_order_creates_correct_order_count(self):
|
||||||
|
# """
|
||||||
|
# Ensure creating orders from product only creates 1 order
|
||||||
|
# """
|
||||||
|
|
||||||
|
# # One order
|
||||||
|
# p = SampleOneTimeProduct.objects.create(owner=self.user)
|
||||||
|
# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||||
|
|
||||||
|
# order_count = Order.objects.filter(owner=self.user).count()
|
||||||
|
# self.assertEqual(order_count, 1)
|
||||||
|
|
||||||
|
# # One more order
|
||||||
|
# p = SampleRecurringProduct.objects.create(owner=self.user)
|
||||||
|
# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||||
|
|
||||||
|
# order_count = Order.objects.filter(owner=self.user).count()
|
||||||
|
# self.assertEqual(order_count, 2)
|
||||||
|
|
||||||
|
# # Should create 2 orders
|
||||||
|
# p = SampleRecurringProductOneTimeFee.objects.create(owner=self.user)
|
||||||
|
# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||||
|
|
||||||
|
# order_count = Order.objects.filter(owner=self.user).count()
|
||||||
|
# self.assertEqual(order_count, 4)
|
||||||
|
|
||||||
|
|
||||||
|
# def test_update_recurring_order(self):
|
||||||
|
# """
|
||||||
|
# Ensure creating orders from product only creates 1 order
|
||||||
|
# """
|
||||||
|
|
||||||
|
# p = SampleRecurringProduct.objects.create(owner=self.user)
|
||||||
|
# p.create_order(timezone.make_aware(datetime.datetime(2020,3,3)))
|
||||||
|
|
||||||
|
# p.create_or_update_recurring_order(timezone.make_aware(datetime.datetime(2020,3,4)))
|
||||||
|
|
||||||
|
# # 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')
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_user_only_inactive_address(self):
|
||||||
|
"""
|
||||||
|
Raise an error, when there is no active address
|
||||||
|
"""
|
||||||
|
|
||||||
|
ba = BillingAddress.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
organization = 'Test org',
|
||||||
|
street="unknown",
|
||||||
|
city="unknown",
|
||||||
|
postal_code="somewhere else",
|
||||||
|
active=False)
|
||||||
|
|
||||||
|
self.assertRaises(uncloud_pay.models.BillingAddress.DoesNotExist,
|
||||||
|
BillingAddress.get_address_for,
|
||||||
|
self.user)
|
||||||
|
|
||||||
|
def test_find_active_address(self):
|
||||||
|
"""
|
||||||
|
Find the active address
|
||||||
|
"""
|
||||||
|
|
||||||
|
ba = BillingAddress.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
organization = 'Test org',
|
||||||
|
street="unknown",
|
||||||
|
city="unknown",
|
||||||
|
postal_code="unknown",
|
||||||
|
active=True)
|
||||||
|
|
||||||
|
|
||||||
|
self.assertEqual(BillingAddress.get_address_for(self.user), ba)
|
||||||
|
|
||||||
|
def test_find_right_address_with_multiple_addresses(self):
|
||||||
|
"""
|
||||||
|
Find the active address only, skip inactive
|
||||||
|
"""
|
||||||
|
|
||||||
|
ba = BillingAddress.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
organization = 'Test org',
|
||||||
|
street="unknown",
|
||||||
|
city="unknown",
|
||||||
|
postal_code="unknown",
|
||||||
|
active=True)
|
||||||
|
|
||||||
|
ba2 = BillingAddress.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
organization = 'Test org',
|
||||||
|
street="unknown",
|
||||||
|
city="unknown",
|
||||||
|
postal_code="somewhere else",
|
||||||
|
active=False)
|
||||||
|
|
||||||
|
|
||||||
|
self.assertEqual(BillingAddress.get_address_for(self.user), ba)
|
||||||
|
|
||||||
|
def test_change_addresses(self):
|
||||||
|
"""
|
||||||
|
Switch the active address
|
||||||
|
"""
|
||||||
|
|
||||||
|
ba = BillingAddress.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
organization = 'Test org',
|
||||||
|
street="unknown",
|
||||||
|
city="unknown",
|
||||||
|
postal_code="unknown",
|
||||||
|
active=True)
|
||||||
|
|
||||||
|
self.assertEqual(BillingAddress.get_address_for(self.user), ba)
|
||||||
|
|
||||||
|
ba.active=False
|
||||||
|
ba.save()
|
||||||
|
|
||||||
|
ba2 = BillingAddress.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
organization = 'Test org',
|
||||||
|
street="unknown",
|
||||||
|
city="unknown",
|
||||||
|
postal_code="somewhere else",
|
||||||
|
active=True)
|
||||||
|
|
||||||
|
self.assertEqual(BillingAddress.get_address_for(self.user), ba2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class BillTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user_without_address = get_user_model().objects.create(
|
||||||
|
username='no_home_person',
|
||||||
|
email='far.away@domain.tld')
|
||||||
|
|
||||||
|
self.user = get_user_model().objects.create(
|
||||||
|
username='jdoe',
|
||||||
|
email='john.doe@domain.tld')
|
||||||
|
|
||||||
|
self.recurring_user = get_user_model().objects.create(
|
||||||
|
username='recurrent_product_user',
|
||||||
|
email='jane.doe@domain.tld')
|
||||||
|
|
||||||
|
self.user_addr = BillingAddress.objects.create(
|
||||||
|
owner=self.user,
|
||||||
|
organization = 'Test org',
|
||||||
|
street="unknown",
|
||||||
|
city="unknown",
|
||||||
|
postal_code="unknown",
|
||||||
|
active=True)
|
||||||
|
|
||||||
|
self.recurring_user_addr = BillingAddress.objects.create(
|
||||||
|
owner=self.recurring_user,
|
||||||
|
organization = 'Test org',
|
||||||
|
street="Somewhere",
|
||||||
|
city="Else",
|
||||||
|
postal_code="unknown",
|
||||||
|
active=True)
|
||||||
|
|
||||||
|
self.order_meta = {}
|
||||||
|
self.order_meta[1] = {
|
||||||
|
'starting_date': timezone.make_aware(datetime.datetime(2020,3,3)),
|
||||||
|
'ending_date': timezone.make_aware(datetime.datetime(2020,4,17)),
|
||||||
|
'price': 15,
|
||||||
|
'description': 'One chocolate bar'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.one_time_order = Order.objects.create(
|
||||||
|
owner=self.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))
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
# used for generating multiple bills
|
||||||
|
self.bill_dates = [
|
||||||
|
timezone.make_aware(datetime.datetime(2020,3,31)),
|
||||||
|
timezone.make_aware(datetime.datetime(2020,4,30)),
|
||||||
|
timezone.make_aware(datetime.datetime(2020,5,31)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_bill_one_time_one_bill_record(self):
|
||||||
|
"""
|
||||||
|
Ensure there is only 1 bill record per order
|
||||||
|
"""
|
||||||
|
|
||||||
|
bill = Bill.create_next_bill_for_user_address(self.user_addr)
|
||||||
|
|
||||||
|
self.assertEqual(self.one_time_order.billrecord_set.count(), 1)
|
||||||
|
|
||||||
|
def test_bill_sum_onetime(self):
|
||||||
|
"""
|
||||||
|
Check the bill sum for a single one time order
|
||||||
|
"""
|
||||||
|
|
||||||
|
bill = Bill.create_next_bill_for_user_address(self.user_addr)
|
||||||
|
self.assertEqual(bill.sum, self.order_meta[1]['price'])
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.assertEqual(self.recurring_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
|
||||||
|
"""
|
||||||
|
|
||||||
|
for ending_date in self.bill_dates:
|
||||||
|
b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date)
|
||||||
|
b.close()
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Ensure multiple bills are created if orders exist with different billing addresses
|
||||||
|
"""
|
||||||
|
|
||||||
|
username="lotsofplaces"
|
||||||
|
multi_addr_user = get_user_model().objects.create(
|
||||||
|
username=username,
|
||||||
|
email=f"{username}@example.org")
|
||||||
|
|
||||||
|
user_addr1 = BillingAddress.objects.create(
|
||||||
|
owner=multi_addr_user,
|
||||||
|
organization = 'Test org',
|
||||||
|
street="unknown",
|
||||||
|
city="unknown",
|
||||||
|
postal_code="unknown",
|
||||||
|
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))
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
self.assertEqual(len(bills), 2)
|
||||||
|
|
||||||
|
|
||||||
|
# TO BE IMPLEMENTED -- once orders can be marked as "done" / "inactive" / "not for billing"
|
||||||
|
# def test_skip_disabled_orders(self):
|
||||||
|
# """
|
||||||
|
# Ensure that a bill only considers "active" orders
|
||||||
|
# """
|
||||||
|
|
||||||
|
# self.assertEqual(1, 2)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
# def setUp(self):
|
||||||
|
# self.user = get_user_model().objects.create(
|
||||||
|
# username='jdoe',
|
||||||
|
# email='john.doe@domain.tld')
|
||||||
|
# self.billing_address = BillingAddress.objects.create(
|
||||||
|
# owner=self.user,
|
||||||
|
# street="unknown",
|
||||||
|
# city="unknown",
|
||||||
|
# postal_code="unknown")
|
||||||
|
|
||||||
|
# def test_basic_monthly_billing(self):
|
||||||
|
# one_time_price = 10
|
||||||
|
# recurring_price = 20
|
||||||
|
# description = "Test Product 1"
|
||||||
|
|
||||||
|
# # Three months: full, full, partial.
|
||||||
|
# # starting_date = datetime.fromisoformat('2020-03-01')
|
||||||
|
# starting_date = datetime(2020,3,1)
|
||||||
|
# ending_date = datetime(2020,5,8)
|
||||||
|
|
||||||
|
# # Create order to be billed.
|
||||||
|
# order = Order.objects.create(
|
||||||
|
# owner=self.user,
|
||||||
|
# starting_date=starting_date,
|
||||||
|
# ending_date=ending_date,
|
||||||
|
# recurring_period=RecurringPeriod.PER_30D,
|
||||||
|
# recurring_price=recurring_price,
|
||||||
|
# one_time_price=one_time_price,
|
||||||
|
# description=description,
|
||||||
|
# billing_address=self.billing_address)
|
||||||
|
|
||||||
|
# # Generate & check bill for first month: full recurring_price + setup.
|
||||||
|
# first_month_bills = order.generate_initial_bill()
|
||||||
|
# self.assertEqual(len(first_month_bills), 1)
|
||||||
|
# self.assertEqual(first_month_bills[0].amount, one_time_price + recurring_price)
|
||||||
|
|
||||||
|
# # Generate & check bill for second month: full recurring_price.
|
||||||
|
# second_month_bills = Bill.generate_for(2020, 4, self.user)
|
||||||
|
# self.assertEqual(len(second_month_bills), 1)
|
||||||
|
# self.assertEqual(second_month_bills[0].amount, recurring_price)
|
||||||
|
|
||||||
|
# # Generate & check bill for third and last month: partial recurring_price.
|
||||||
|
# third_month_bills = Bill.generate_for(2020, 5, self.user)
|
||||||
|
# self.assertEqual(len(third_month_bills), 1)
|
||||||
|
# # 31 days in May.
|
||||||
|
# self.assertEqual(float(third_month_bills[0].amount),
|
||||||
|
# round(round((7/31), AMOUNT_DECIMALS) * recurring_price, AMOUNT_DECIMALS))
|
||||||
|
|
||||||
|
# # Check that running Bill.generate_for() twice does not create duplicates.
|
||||||
|
# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0)
|
||||||
|
|
||||||
|
# def test_basic_yearly_billing(self):
|
||||||
|
# one_time_price = 10
|
||||||
|
# recurring_price = 150
|
||||||
|
# description = "Test Product 1"
|
||||||
|
|
||||||
|
# starting_date = datetime.fromisoformat('2020-03-31T08:05:23')
|
||||||
|
|
||||||
|
# # Create order to be billed.
|
||||||
|
# order = Order.objects.create(
|
||||||
|
# owner=self.user,
|
||||||
|
# starting_date=starting_date,
|
||||||
|
# recurring_period=RecurringPeriod.PER_365D,
|
||||||
|
# recurring_price=recurring_price,
|
||||||
|
# one_time_price=one_time_price,
|
||||||
|
# description=description,
|
||||||
|
# billing_address=self.billing_address)
|
||||||
|
|
||||||
|
# # Generate & check bill for first year: recurring_price + setup.
|
||||||
|
# first_year_bills = order.generate_initial_bill()
|
||||||
|
# self.assertEqual(len(first_year_bills), 1)
|
||||||
|
# self.assertEqual(first_year_bills[0].starting_date.date(),
|
||||||
|
# date.fromisoformat('2020-03-31'))
|
||||||
|
# self.assertEqual(first_year_bills[0].ending_date.date(),
|
||||||
|
# date.fromisoformat('2021-03-30'))
|
||||||
|
# self.assertEqual(first_year_bills[0].amount,
|
||||||
|
# recurring_price + one_time_price)
|
||||||
|
|
||||||
|
# # Generate & check bill for second year: recurring_price.
|
||||||
|
# second_year_bills = Bill.generate_for(2021, 3, self.user)
|
||||||
|
# self.assertEqual(len(second_year_bills), 1)
|
||||||
|
# self.assertEqual(second_year_bills[0].starting_date.date(),
|
||||||
|
# date.fromisoformat('2021-03-31'))
|
||||||
|
# self.assertEqual(second_year_bills[0].ending_date.date(),
|
||||||
|
# date.fromisoformat('2022-03-30'))
|
||||||
|
# self.assertEqual(second_year_bills[0].amount, recurring_price)
|
||||||
|
|
||||||
|
# # Check that running Bill.generate_for() twice does not create duplicates.
|
||||||
|
# self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0)
|
||||||
|
# self.assertEqual(len(Bill.generate_for(2020, 4, self.user)), 0)
|
||||||
|
# self.assertEqual(len(Bill.generate_for(2020, 2, self.user)), 0)
|
||||||
|
# self.assertEqual(len(Bill.generate_for(2021, 3, self.user)), 0)
|
||||||
|
|
||||||
|
# def test_basic_hourly_billing(self):
|
||||||
|
# one_time_price = 10
|
||||||
|
# recurring_price = 1.4
|
||||||
|
# description = "Test Product 1"
|
||||||
|
|
||||||
|
# starting_date = datetime.fromisoformat('2020-03-31T08:05:23')
|
||||||
|
# ending_date = datetime.fromisoformat('2020-04-01T11:13:32')
|
||||||
|
|
||||||
|
# # Create order to be billed.
|
||||||
|
# order = Order.objects.create(
|
||||||
|
# owner=self.user,
|
||||||
|
# starting_date=starting_date,
|
||||||
|
# ending_date=ending_date,
|
||||||
|
# recurring_period=RecurringPeriod.PER_HOUR,
|
||||||
|
# recurring_price=recurring_price,
|
||||||
|
# one_time_price=one_time_price,
|
||||||
|
# description=description,
|
||||||
|
# billing_address=self.billing_address)
|
||||||
|
|
||||||
|
# # Generate & check bill for first month: recurring_price + setup.
|
||||||
|
# first_month_bills = order.generate_initial_bill()
|
||||||
|
# self.assertEqual(len(first_month_bills), 1)
|
||||||
|
# self.assertEqual(float(first_month_bills[0].amount),
|
||||||
|
# round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price)
|
||||||
|
|
||||||
|
# # Generate & check bill for first month: recurring_price.
|
||||||
|
# second_month_bills = Bill.generate_for(2020, 4, self.user)
|
||||||
|
# self.assertEqual(len(second_month_bills), 1)
|
||||||
|
# self.assertEqual(float(second_month_bills[0].amount),
|
||||||
|
# round(12 * recurring_price, AMOUNT_DECIMALS))
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue