forked from uncloud/uncloud
Fix first test case / billing
This commit is contained in:
parent
c26ff253de
commit
2e74661702
3 changed files with 111 additions and 79 deletions
|
@ -371,12 +371,6 @@ class Product(UncloudModel):
|
||||||
currency=Currency.CHF,
|
currency=Currency.CHF,
|
||||||
config={
|
config={
|
||||||
'features': {
|
'features': {
|
||||||
'base':
|
|
||||||
{ 'min': 1,
|
|
||||||
'max': 1,
|
|
||||||
'one_time_price_per_unit': 0,
|
|
||||||
'recurring_price_per_unit': 8
|
|
||||||
},
|
|
||||||
'cores':
|
'cores':
|
||||||
{ 'min': 1,
|
{ 'min': 1,
|
||||||
'max': 48,
|
'max': 48,
|
||||||
|
@ -390,9 +384,9 @@ class Product(UncloudModel):
|
||||||
'recurring_price_per_unit': 4
|
'recurring_price_per_unit': 4
|
||||||
},
|
},
|
||||||
'ssd_gb':
|
'ssd_gb':
|
||||||
{ 'min': 1,
|
{ 'min': 10,
|
||||||
'one_time_price_per_unit': 0,
|
'one_time_price_per_unit': 0,
|
||||||
'recurring_price_per_unit': 3.5
|
'recurring_price_per_unit': 0.35
|
||||||
},
|
},
|
||||||
'hdd_gb':
|
'hdd_gb':
|
||||||
{ 'min': 0,
|
{ 'min': 0,
|
||||||
|
@ -673,7 +667,7 @@ class Order(models.Model):
|
||||||
One time orders have a recurring period of 0, so this work universally
|
One time orders have a recurring period of 0, so this work universally
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.starting_date + datetime.timedelta(seconds=self.recurring_period)
|
return self.starting_date + datetime.timedelta(seconds=self.recurring_period.duration_seconds)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def next_cancel_or_downgrade_date(self):
|
def next_cancel_or_downgrade_date(self):
|
||||||
|
@ -683,7 +677,7 @@ class Order(models.Model):
|
||||||
or cancelling.
|
or cancelling.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if self.recurring_period > 0:
|
if self.recurring_period.seconds > 0:
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
delta = now - self.starting_date
|
delta = now - self.starting_date
|
||||||
|
|
||||||
|
@ -781,11 +775,14 @@ class Order(models.Model):
|
||||||
|
|
||||||
new_order = self.__class__(owner=self.owner,
|
new_order = self.__class__(owner=self.owner,
|
||||||
billing_address=self.billing_address,
|
billing_address=self.billing_address,
|
||||||
|
description=self.description,
|
||||||
product=self.product,
|
product=self.product,
|
||||||
|
config=config,
|
||||||
starting_date=starting_date,
|
starting_date=starting_date,
|
||||||
config=config)
|
currency=self.currency
|
||||||
|
)
|
||||||
|
|
||||||
(new_order_one_time_price, new_order_recurring_price) = new_order.prices
|
(new_order.one_time_price, new_order.recurring_price, new_order.config) = new_order.calculate_prices_and_config()
|
||||||
|
|
||||||
new_order.replaces = self
|
new_order.replaces = self
|
||||||
new_order.save()
|
new_order.save()
|
||||||
|
@ -793,12 +790,7 @@ class Order(models.Model):
|
||||||
self.ending_date = end_before(new_order.starting_date)
|
self.ending_date = end_before(new_order.starting_date)
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
return new_order
|
||||||
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")
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def create_bill_record(self, bill):
|
def create_bill_record(self, bill):
|
||||||
|
@ -871,12 +863,22 @@ class Order(models.Model):
|
||||||
ending_date=ending_date,
|
ending_date=ending_date,
|
||||||
is_recurring_record=True)
|
is_recurring_record=True)
|
||||||
|
|
||||||
@property
|
def calculate_prices_and_config(self):
|
||||||
def prices(self):
|
|
||||||
one_time_price = 0
|
one_time_price = 0
|
||||||
recurring_price = 0
|
recurring_price = 0
|
||||||
|
|
||||||
# FIXME: adjust to the selected recurring_period
|
if self.config:
|
||||||
|
config = self.config
|
||||||
|
|
||||||
|
if 'features' not in self.config:
|
||||||
|
self.config['features'] = {}
|
||||||
|
|
||||||
|
else:
|
||||||
|
config = {
|
||||||
|
'features': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# FIXME: adjust prices to the selected recurring_period to the
|
||||||
|
|
||||||
if 'features' in self.product.config:
|
if 'features' in self.product.config:
|
||||||
for feature in self.product.config['features']:
|
for feature in self.product.config['features']:
|
||||||
|
@ -887,37 +889,47 @@ class Order(models.Model):
|
||||||
# We might not even have 'features' cannot use .get() on it
|
# We might not even have 'features' cannot use .get() on it
|
||||||
try:
|
try:
|
||||||
value = self.config['features'][feature]
|
value = self.config['features'][feature]
|
||||||
except KeyError:
|
except (KeyError, TypeError):
|
||||||
value = self.product.config['features'][feature]['min']
|
value = self.product.config['features'][feature]['min']
|
||||||
|
|
||||||
# Set max to current value if not specified
|
# Set max to current value if not specified
|
||||||
max_val = self.product.config['features'][feature].get('max', value)
|
max_val = self.product.config['features'][feature].get('max', value)
|
||||||
|
|
||||||
|
|
||||||
if value < min_val or value > max_val:
|
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}")
|
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
|
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
|
recurring_price += self.product.config['features'][feature]['recurring_price_per_unit'] * value
|
||||||
|
config['features'][feature] = value
|
||||||
|
|
||||||
return (one_time_price, recurring_price)
|
return (one_time_price, recurring_price, config)
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
# Calculate the price of the order when we create it
|
# Calculate the price of the order when we create it
|
||||||
# IMMUTABLE fields -- need to create new order to modify them
|
# IMMUTABLE fields -- need to create new order to modify them
|
||||||
# However this is not enforced here...
|
# However this is not enforced here...
|
||||||
if self._state.adding:
|
if self._state.adding:
|
||||||
(self.one_time_price, self.recurring_price) = self.prices
|
(self.one_time_price, self.recurring_price, self.config) = self.calculate_prices_and_config()
|
||||||
|
|
||||||
if self.recurring_period_id is None:
|
if self.recurring_period_id is None:
|
||||||
self.recurring_period = self.product.default_recurring_period
|
self.recurring_period = self.product.default_recurring_period
|
||||||
|
|
||||||
# FIXME: ensure the recurring period is defined in the product
|
try:
|
||||||
|
prod_period = self.product.recurring_periods.get(producttorecurringperiod__recurring_period=self.recurring_period)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise ValidationError(f"Recurring Period {self.recurring_period} not allowed for product {self.product}")
|
||||||
|
|
||||||
|
if self.ending_date and self.ending_date < self.starting_date:
|
||||||
|
raise ValidationError("End date cannot be before starting date")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Order {self.id} from {self.owner}: {self.product}"
|
return f"Order {self.id}: {self.description} {self.config}"
|
||||||
|
|
||||||
class Bill(models.Model):
|
class Bill(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -988,14 +1000,33 @@ class Bill(models.Model):
|
||||||
return bills
|
return bills
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_or_create_bill(cls, billing_address):
|
def create_next_bill_for_user_address(cls, billing_address, ending_date=None):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Create the next bill for a specific billing address of a user
|
||||||
|
"""
|
||||||
|
|
||||||
|
owner = billing_address.owner
|
||||||
|
|
||||||
|
all_orders = Order.objects.filter(owner=owner,
|
||||||
|
billing_address=billing_address).order_by('id')
|
||||||
|
|
||||||
|
bill = cls.get_or_create_bill(billing_address, ending_date=ending_date)
|
||||||
|
|
||||||
|
for order in all_orders:
|
||||||
|
order.create_bill_record(bill)
|
||||||
|
|
||||||
|
return bill
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create_bill(cls, billing_address, ending_date=None):
|
||||||
last_bill = cls.objects.filter(billing_address=billing_address).order_by('id').last()
|
last_bill = cls.objects.filter(billing_address=billing_address).order_by('id').last()
|
||||||
|
|
||||||
all_orders = Order.objects.filter(billing_address=billing_address).order_by('id')
|
all_orders = Order.objects.filter(billing_address=billing_address).order_by('id')
|
||||||
first_order = all_orders.first()
|
first_order = all_orders.first()
|
||||||
|
|
||||||
bill = None
|
bill = None
|
||||||
ending_date = None
|
|
||||||
|
|
||||||
# Get date & bill from previous bill, if it exists
|
# Get date & bill from previous bill, if it exists
|
||||||
if last_bill:
|
if last_bill:
|
||||||
|
@ -1025,27 +1056,6 @@ class Bill(models.Model):
|
||||||
|
|
||||||
return bill
|
return bill
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_next_bill_for_user_address(cls,
|
|
||||||
billing_address,
|
|
||||||
ending_date=None):
|
|
||||||
|
|
||||||
"""
|
|
||||||
Create the next bill for a specific billing address of a user
|
|
||||||
"""
|
|
||||||
|
|
||||||
owner = billing_address.owner
|
|
||||||
|
|
||||||
all_orders = Order.objects.filter(owner=owner,
|
|
||||||
billing_address=billing_address).order_by('id')
|
|
||||||
|
|
||||||
bill = cls.get_or_create_bill(billing_address)
|
|
||||||
|
|
||||||
for order in all_orders:
|
|
||||||
order.create_bill_record(bill)
|
|
||||||
|
|
||||||
return bill
|
|
||||||
|
|
||||||
# @classmethod
|
# @classmethod
|
||||||
# def create_bill_records_for_recurring_orders(cls, bill):
|
# def create_bill_records_for_recurring_orders(cls, bill):
|
||||||
# """
|
# """
|
||||||
|
@ -1100,6 +1110,8 @@ class Bill(models.Model):
|
||||||
if not last_bill.is_final:
|
if not last_bill.is_final:
|
||||||
bill = last_bill
|
bill = last_bill
|
||||||
starting_date = last_bill.starting_date
|
starting_date = last_bill.starting_date
|
||||||
|
|
||||||
|
# FIXME: take given (parameter) or existing ending_date?
|
||||||
ending_date = bill.ending_date
|
ending_date = bill.ending_date
|
||||||
else:
|
else:
|
||||||
starting_date = last_bill.ending_date + datetime.timedelta(seconds=1)
|
starting_date = last_bill.ending_date + datetime.timedelta(seconds=1)
|
||||||
|
@ -1225,7 +1237,7 @@ class BillRecord(models.Model):
|
||||||
|
|
||||||
record_delta = self.ending_date - self.starting_date
|
record_delta = self.ending_date - self.starting_date
|
||||||
|
|
||||||
return record_delta.total_seconds()/self.order.recurring_period
|
return record_delta.total_seconds()/self.order.recurring_period.duration_seconds
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sum(self):
|
def sum(self):
|
||||||
|
|
|
@ -36,7 +36,6 @@
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 600px;
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding-top: 40px;
|
padding-top: 40px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 15px;
|
||||||
|
|
|
@ -13,8 +13,8 @@ chocolate_product_config = {
|
||||||
'gramm':
|
'gramm':
|
||||||
{ 'min': 100,
|
{ 'min': 100,
|
||||||
'max': 5000,
|
'max': 5000,
|
||||||
'one_time_price': 0.2,
|
'one_time_price_per_unit': 0.2,
|
||||||
'recurring_price': 0
|
'recurring_price_per_unit': 0
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -25,21 +25,21 @@ chocolate_order_config = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chocolate_one_time_price = chocolate_order_config['features']['gramm'] * chocolate_product_config['features']['gramm']['one_time_price']
|
chocolate_one_time_price = chocolate_order_config['features']['gramm'] * chocolate_product_config['features']['gramm']['one_time_price_per_unit']
|
||||||
|
|
||||||
vm_product_config = {
|
vm_product_config = {
|
||||||
'features': {
|
'features': {
|
||||||
'cores':
|
'cores':
|
||||||
{ 'min': 1,
|
{ 'min': 1,
|
||||||
'max': 48,
|
'max': 48,
|
||||||
'one_time_price': 0,
|
'one_time_price_per_unit': 0,
|
||||||
'recurring_price': 4
|
'recurring_price_per_unit': 4
|
||||||
},
|
},
|
||||||
'ram_gb':
|
'ram_gb':
|
||||||
{ 'min': 1,
|
{ 'min': 1,
|
||||||
'max': 256,
|
'max': 256,
|
||||||
'one_time_price': 0,
|
'one_time_price_per_unit': 0,
|
||||||
'recurring_price': 4
|
'recurring_price_per_unit': 4
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -94,8 +94,10 @@ class ProductTestCase(TestCase):
|
||||||
|
|
||||||
p = Product.objects.create(name="Testproduct",
|
p = Product.objects.create(name="Testproduct",
|
||||||
description="Only for testing",
|
description="Only for testing",
|
||||||
config=vm_product_config,
|
config=vm_product_config)
|
||||||
default_recurring_period=self.default_recurring_period)
|
|
||||||
|
p.recurring_periods.add(self.default_recurring_period,
|
||||||
|
through_defaults= { 'is_default': True })
|
||||||
|
|
||||||
|
|
||||||
class OrderTestCase(TestCase):
|
class OrderTestCase(TestCase):
|
||||||
|
@ -116,24 +118,36 @@ class OrderTestCase(TestCase):
|
||||||
postal_code="somewhere else",
|
postal_code="somewhere else",
|
||||||
active=True)
|
active=True)
|
||||||
|
|
||||||
|
self.product = Product.objects.create(name="Testproduct",
|
||||||
|
description="Only for testing",
|
||||||
|
config=vm_product_config)
|
||||||
|
|
||||||
RecurringPeriod.populate_db_defaults()
|
RecurringPeriod.populate_db_defaults()
|
||||||
self.default_recurring_period = RecurringPeriod.objects.get(name="Per 30 days")
|
self.default_recurring_period = RecurringPeriod.objects.get(name="Per 30 days")
|
||||||
|
|
||||||
|
self.product.recurring_periods.add(self.default_recurring_period,
|
||||||
|
through_defaults= { 'is_default': True })
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_invalid_recurring_period(self):
|
||||||
|
"""
|
||||||
|
Order a products with a recurringperiod that is not added to the product
|
||||||
|
"""
|
||||||
|
|
||||||
|
o = Order.objects.create(owner=self.user,
|
||||||
|
billing_address=self.ba,
|
||||||
|
product=self.product,
|
||||||
|
config=vm_order_config)
|
||||||
|
|
||||||
|
|
||||||
def test_order_product(self):
|
def test_order_product(self):
|
||||||
"""
|
"""
|
||||||
Order a product, ensure the order has correct price setup
|
Order a product, ensure the order has correct price setup
|
||||||
"""
|
"""
|
||||||
|
|
||||||
p = Product.objects.create(name="Testproduct",
|
|
||||||
description="Only for testing",
|
|
||||||
config=vm_product_config,
|
|
||||||
default_recurring_period=self.default_recurring_period)
|
|
||||||
|
|
||||||
o = Order.objects.create(owner=self.user,
|
o = Order.objects.create(owner=self.user,
|
||||||
billing_address=self.ba,
|
billing_address=self.ba,
|
||||||
product=p,
|
product=self.product)
|
||||||
config=vm_order_config)
|
|
||||||
|
|
||||||
self.assertEqual(o.one_time_price, 0)
|
self.assertEqual(o.one_time_price, 0)
|
||||||
self.assertEqual(o.recurring_price, 16)
|
self.assertEqual(o.recurring_price, 16)
|
||||||
|
@ -144,19 +158,12 @@ class OrderTestCase(TestCase):
|
||||||
- a new order is created
|
- a new order is created
|
||||||
- the price is correct in the new order
|
- the price is correct in the new order
|
||||||
"""
|
"""
|
||||||
|
|
||||||
p = Product.objects.create(name="Testproduct",
|
|
||||||
description="Only for testing",
|
|
||||||
config=vm_product_config,
|
|
||||||
default_recurring_period=self.default_recurring_period)
|
|
||||||
|
|
||||||
order1 = Order.objects.create(owner=self.user,
|
order1 = Order.objects.create(owner=self.user,
|
||||||
billing_address=self.ba,
|
billing_address=self.ba,
|
||||||
product=p,
|
product=self.product,
|
||||||
config=vm_order_config)
|
config=vm_order_config)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.assertEqual(order1.one_time_price, 0)
|
self.assertEqual(order1.one_time_price, 0)
|
||||||
self.assertEqual(order1.recurring_price, 16)
|
self.assertEqual(order1.recurring_price, 16)
|
||||||
|
|
||||||
|
@ -182,13 +189,15 @@ class ModifyOrderTestCase(TestCase):
|
||||||
postal_code="somewhere else",
|
postal_code="somewhere else",
|
||||||
active=True)
|
active=True)
|
||||||
|
|
||||||
|
self.product = Product.objects.create(name="Testproduct",
|
||||||
|
description="Only for testing",
|
||||||
|
config=vm_product_config)
|
||||||
|
|
||||||
RecurringPeriod.populate_db_defaults()
|
RecurringPeriod.populate_db_defaults()
|
||||||
self.default_recurring_period = RecurringPeriod.objects.get(name="Per 30 days")
|
self.default_recurring_period = RecurringPeriod.objects.get(name="Per 30 days")
|
||||||
|
|
||||||
self.product = Product.objects.create(name="Testproduct",
|
self.product.recurring_periods.add(self.default_recurring_period,
|
||||||
description="Only for testing",
|
through_defaults= { 'is_default': True })
|
||||||
config=vm_product_config,
|
|
||||||
default_recurring_period=self.default_recurring_period)
|
|
||||||
|
|
||||||
|
|
||||||
def test_change_order(self):
|
def test_change_order(self):
|
||||||
|
@ -468,6 +477,18 @@ class BillTestCase(TestCase):
|
||||||
config=vm_product_config)
|
config=vm_product_config)
|
||||||
|
|
||||||
|
|
||||||
|
RecurringPeriod.populate_db_defaults()
|
||||||
|
self.default_recurring_period = RecurringPeriod.objects.get(name="Per 30 days")
|
||||||
|
|
||||||
|
self.onetime_recurring_period = RecurringPeriod.objects.get(name="Onetime")
|
||||||
|
|
||||||
|
self.chocolate.recurring_periods.add(self.onetime_recurring_period,
|
||||||
|
through_defaults= { 'is_default': True })
|
||||||
|
|
||||||
|
self.vm.recurring_periods.add(self.default_recurring_period,
|
||||||
|
through_defaults= { 'is_default': True })
|
||||||
|
|
||||||
|
|
||||||
# used for generating multiple bills
|
# used for generating multiple bills
|
||||||
self.bill_dates = [
|
self.bill_dates = [
|
||||||
timezone.make_aware(datetime.datetime(2020,3,31)),
|
timezone.make_aware(datetime.datetime(2020,3,31)),
|
||||||
|
|
Loading…
Reference in a new issue