Fix first test case / billing

This commit is contained in:
Nico Schottelius 2020-10-06 23:14:32 +02:00
parent c26ff253de
commit 2e74661702
3 changed files with 111 additions and 79 deletions

View File

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

View File

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

View File

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