From 2e746617021e0ce607f17babf6b980fbec90ddb7 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Tue, 6 Oct 2020 23:14:32 +0200 Subject: [PATCH] Fix first test case / billing --- uncloud_pay/models.py | 112 ++++++++++++++++------------- uncloud_pay/templates/bill.html.j2 | 1 - uncloud_pay/tests.py | 77 ++++++++++++-------- 3 files changed, 111 insertions(+), 79 deletions(-) diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py index 6962601..15613a7 100644 --- a/uncloud_pay/models.py +++ b/uncloud_pay/models.py @@ -371,12 +371,6 @@ class Product(UncloudModel): currency=Currency.CHF, config={ 'features': { - 'base': - { 'min': 1, - 'max': 1, - 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 8 - }, 'cores': { 'min': 1, 'max': 48, @@ -390,9 +384,9 @@ class Product(UncloudModel): 'recurring_price_per_unit': 4 }, 'ssd_gb': - { 'min': 1, + { 'min': 10, 'one_time_price_per_unit': 0, - 'recurring_price_per_unit': 3.5 + 'recurring_price_per_unit': 0.35 }, 'hdd_gb': { 'min': 0, @@ -673,7 +667,7 @@ class Order(models.Model): 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 def next_cancel_or_downgrade_date(self): @@ -683,7 +677,7 @@ class Order(models.Model): or cancelling. """ - if self.recurring_period > 0: + if self.recurring_period.seconds > 0: now = timezone.now() delta = now - self.starting_date @@ -781,11 +775,14 @@ class Order(models.Model): new_order = self.__class__(owner=self.owner, billing_address=self.billing_address, + description=self.description, product=self.product, + config=config, 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.save() @@ -793,12 +790,7 @@ class Order(models.Model): 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") - - super().save(*args, **kwargs) + return new_order def create_bill_record(self, bill): @@ -871,12 +863,22 @@ class Order(models.Model): ending_date=ending_date, is_recurring_record=True) - @property - def prices(self): + def calculate_prices_and_config(self): one_time_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: 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 try: value = self.config['features'][feature] - except KeyError: + except (KeyError, TypeError): value = self.product.config['features'][feature]['min'] # Set max to current value if not specified max_val = self.product.config['features'][feature].get('max', value) + if value < min_val or value > max_val: raise ValidationError(f"Feature '{feature}' must be at least {min_val} and at maximum {max_val}. Value is: {value}") one_time_price += self.product.config['features'][feature]['one_time_price_per_unit'] * value recurring_price += self.product.config['features'][feature]['recurring_price_per_unit'] * value + config['features'][feature] = value - return (one_time_price, recurring_price) + return (one_time_price, recurring_price, config) 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, 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: 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) 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): """ @@ -988,14 +1000,33 @@ class Bill(models.Model): return bills @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() all_orders = Order.objects.filter(billing_address=billing_address).order_by('id') first_order = all_orders.first() bill = None - ending_date = None # Get date & bill from previous bill, if it exists if last_bill: @@ -1025,27 +1056,6 @@ class Bill(models.Model): 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 # def create_bill_records_for_recurring_orders(cls, bill): # """ @@ -1100,6 +1110,8 @@ class Bill(models.Model): if not last_bill.is_final: bill = last_bill starting_date = last_bill.starting_date + + # FIXME: take given (parameter) or existing ending_date? ending_date = bill.ending_date else: 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 - return record_delta.total_seconds()/self.order.recurring_period + return record_delta.total_seconds()/self.order.recurring_period.duration_seconds @property def sum(self): diff --git a/uncloud_pay/templates/bill.html.j2 b/uncloud_pay/templates/bill.html.j2 index 6fdfca8..e3238d3 100644 --- a/uncloud_pay/templates/bill.html.j2 +++ b/uncloud_pay/templates/bill.html.j2 @@ -36,7 +36,6 @@ font-weight: 500; line-height: 1.1; font-size: 14px; - width: 600px; margin: auto; padding-top: 40px; padding-bottom: 15px; diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py index 5bec86f..0ebd11c 100644 --- a/uncloud_pay/tests.py +++ b/uncloud_pay/tests.py @@ -13,8 +13,8 @@ chocolate_product_config = { 'gramm': { 'min': 100, 'max': 5000, - 'one_time_price': 0.2, - 'recurring_price': 0 + 'one_time_price_per_unit': 0.2, + '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 = { 'features': { 'cores': { 'min': 1, 'max': 48, - 'one_time_price': 0, - 'recurring_price': 4 + 'one_time_price_per_unit': 0, + 'recurring_price_per_unit': 4 }, 'ram_gb': { 'min': 1, 'max': 256, - 'one_time_price': 0, - 'recurring_price': 4 + 'one_time_price_per_unit': 0, + 'recurring_price_per_unit': 4 }, }, } @@ -94,8 +94,10 @@ class ProductTestCase(TestCase): p = Product.objects.create(name="Testproduct", description="Only for testing", - config=vm_product_config, - default_recurring_period=self.default_recurring_period) + config=vm_product_config) + + p.recurring_periods.add(self.default_recurring_period, + through_defaults= { 'is_default': True }) class OrderTestCase(TestCase): @@ -116,24 +118,36 @@ class OrderTestCase(TestCase): postal_code="somewhere else", active=True) + self.product = Product.objects.create(name="Testproduct", + description="Only for testing", + config=vm_product_config) + RecurringPeriod.populate_db_defaults() 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): """ 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, billing_address=self.ba, - product=p, - config=vm_order_config) + product=self.product) self.assertEqual(o.one_time_price, 0) self.assertEqual(o.recurring_price, 16) @@ -144,19 +158,12 @@ class OrderTestCase(TestCase): - a new order is created - 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, billing_address=self.ba, - product=p, + product=self.product, config=vm_order_config) - self.assertEqual(order1.one_time_price, 0) self.assertEqual(order1.recurring_price, 16) @@ -182,13 +189,15 @@ class ModifyOrderTestCase(TestCase): postal_code="somewhere else", active=True) + self.product = Product.objects.create(name="Testproduct", + description="Only for testing", + config=vm_product_config) + RecurringPeriod.populate_db_defaults() self.default_recurring_period = RecurringPeriod.objects.get(name="Per 30 days") - self.product = Product.objects.create(name="Testproduct", - description="Only for testing", - config=vm_product_config, - default_recurring_period=self.default_recurring_period) + self.product.recurring_periods.add(self.default_recurring_period, + through_defaults= { 'is_default': True }) def test_change_order(self): @@ -468,6 +477,18 @@ class BillTestCase(TestCase): 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 self.bill_dates = [ timezone.make_aware(datetime.datetime(2020,3,31)),