From 2e746617021e0ce607f17babf6b980fbec90ddb7 Mon Sep 17 00:00:00 2001
From: Nico Schottelius <nico@nico-notebook.schottelius.org>
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)),