From a3696b859ad9eb439c46377cad66d2f4124e7d18 Mon Sep 17 00:00:00 2001
From: Marc <orchestra@localhost>
Date: Thu, 25 Sep 2014 16:28:47 +0000
Subject: [PATCH] TrafficPrepayBillingTest tests passing

---
 orchestra/apps/orders/billing.py              |  1 +
 orchestra/apps/orders/models.py               |  6 +-
 .../orders/tests/functional_tests/tests.py    | 76 +++++++++++--------
 orchestra/apps/services/handlers.py           | 49 +++++++-----
 orchestra/apps/services/models.py             | 31 ++++++--
 5 files changed, 102 insertions(+), 61 deletions(-)

diff --git a/orchestra/apps/orders/billing.py b/orchestra/apps/orders/billing.py
index 5e93479e..39942379 100644
--- a/orchestra/apps/orders/billing.py
+++ b/orchestra/apps/orders/billing.py
@@ -46,6 +46,7 @@ class BillsBackend(object):
     def format_period(self, ini, end):
         ini = ini.strftime("%b, %Y")
         end = (end-datetime.timedelta(seconds=1)).strftime("%b, %Y")
+        # TODO if diff is less than a month: write the month only
         if ini == end:
             return ini
         return _("{ini} to {end}").format(ini=ini, end=end)
diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py
index 5054b2d9..6a7b2eda 100644
--- a/orchestra/apps/orders/models.py
+++ b/orchestra/apps/orders/models.py
@@ -1,3 +1,4 @@
+import datetime
 import decimal
 import logging
 import sys
@@ -167,8 +168,9 @@ class Order(models.Model):
             metrics = self.metrics.filter(updated_on__lt=end, updated_on__gte=ini)
         elif len(args) == 1:
             date = args[0]
-            metrics = self.metrics.filter(updated_on__year=date.year,
-                    updated_on__month=date.month, updated_on__day=date.day)
+            date = datetime.date(year=date.year, month=date.month, day=date.day)
+            date += datetime.timedelta(days=1)
+            metrics = self.metrics.filter(updated_on__lt=date)
         elif not args:
             return self.metrics.latest('updated_on').value
         else:
diff --git a/orchestra/apps/orders/tests/functional_tests/tests.py b/orchestra/apps/orders/tests/functional_tests/tests.py
index 0b1a522d..2dde24ea 100644
--- a/orchestra/apps/orders/tests/functional_tests/tests.py
+++ b/orchestra/apps/orders/tests/functional_tests/tests.py
@@ -330,23 +330,24 @@ class TrafficBillingTest(BaseTrafficBillingTest):
 
 
 class TrafficPrepayBillingTest(BaseTrafficBillingTest):
-    METRIC = "max((account.resources.traffic.used or 0) - getattr(account.miscellaneous.filter(service__name='traffic prepay').last(), 'amount', 0), 0)"
+    METRIC = "max((account.resources.traffic.used or 0) - getattr(account.miscellaneous.filter(is_active=True, service__name='traffic prepay').last(), 'amount', 0), 0)"
     
     def create_prepay_service(self):
         service = Service.objects.create(
             description="Traffic prepay",
             content_type=ContentType.objects.get_for_model(Miscellaneous),
             match="miscellaneous.is_active and miscellaneous.service.name.lower() == 'traffic prepay'",
-            billing_period=Service.ANUAL,
-            billing_point=Service.FIXED_DATE,
+            billing_period=Service.MONTHLY,
+            # make sure full months are always paid
+            billing_point=Service.ON_REGISTER,
             is_fee=False,
             metric="miscellaneous.amount",
-            pricing_period=Service.BILLING_PERIOD,
+            pricing_period=Service.NEVER,
             rate_algorithm=Service.STEP_PRICE,
-            on_cancel=Service.NOTHING, # TODO on_register == NOTHING or make on_cancel generic
+            on_cancel=Service.NOTHING,
             payment_style=Service.PREPAY,
             tax=0,
-            nominal_price=5
+            nominal_price=50
         )
         return service
     
@@ -361,37 +362,43 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest):
         service = self.create_traffic_service()
         prepay_service = self.create_prepay_service()
         account = self.create_account()
-        
         self.create_traffic_resource()
-        prepay = self.create_prepay(10, account=account)
-        self.report_traffic(account, timezone.now(), 10**9)
+        now = timezone.now()
+        
+        prepay = self.create_prepay(10, account=account)
+        bill = account.orders.bill(proforma=True)[0]
+        self.assertEqual(10*50, bill.get_total())
+        
+        self.report_traffic(account, timezone.now(), 10**10)
+        with freeze_time(now+relativedelta(months=1)):
+            bill = account.orders.bill(proforma=True, new_open=True)[0]
+            self.assertEqual(2*10*50 + 0*10, bill.get_total())
+        
+        # TODO dateutils.relativedelta is buggy with fakedatetime
+        # TODO RuntimeWarning: DateTimeField MetricStorage.updated_on received a naive
+        self.report_traffic(account, timezone.now(), 10**10)
+        with freeze_time(now+relativedelta(months=1)):
+            bill = account.orders.bill(proforma=True, new_open=True)[0]
+        self.assertEqual(2*10*50 + 0*10, bill.get_total())
+        
+        self.report_traffic(account, timezone.now(), 10**10)
+        with freeze_time(now+relativedelta(months=1)):
+            bill = account.orders.bill(proforma=True, new_open=True)[0]
+        self.assertEqual(2*10*50 + (30-10-10)*10, bill.get_total())
+        
+        with freeze_time(now+relativedelta(months=2)):
+            self.report_traffic(account, timezone.now(), 10**11)
+        with freeze_time(now+relativedelta(months=1)):
+            bill = account.orders.bill(proforma=True, new_open=True)[0]
+            self.assertEqual(2*10*50 + (30-10-10)*10, bill.get_total())
+        
+        with freeze_time(now+relativedelta(months=3)):
+            bill = account.orders.bill(proforma=True, new_open=True)[0]
+        self.assertEqual(4*10*50 +  (30-10-10)*10 + (100-10-10)*10, bill.get_total())
         
-        print prepay_service.orders.all()
         # TODO metric on the current day! how to solve it consistently?
         # TODO prepay doesnt allow for discount
-        
-        # move into the past
-        # TODO         with patch.object(timezone, 'now', return_value=now+relativedelta(years=1)):
-        delta = datetime.timedelta(days=60)
-        date = (timezone.now()-delta).date()
-        order = service.orders.get()
-        order.registered_on = date
-        order.save()
-        
-        metric = order.metrics.latest()
-        metric.updated_on -= delta
-        metric.save()
-        
-        bills = service.orders.bill(proforma=True)
-        self.assertEqual(0, bills[0].get_total())
-        
-        self.report_traffic(account, date, 10**10*9)
-        metric = order.metrics.latest()
-        metric.updated_on -= delta
-        metric.save()
-        
-        bills = service.orders.bill(proforma=True)
-        self.assertEqual((90-10-10)*10, bills[0].get_total())
+
 
 
 class MailboxBillingTest(BaseBillingTest):
@@ -583,3 +590,6 @@ class PlanBillingTest(BaseBillingTest):
     
     def test_plan(self):
         pass
+
+
+# TODO web disk size
diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py
index 16fc7708..5c4a4686 100644
--- a/orchestra/apps/services/handlers.py
+++ b/orchestra/apps/services/handlers.py
@@ -73,8 +73,7 @@ class ServiceHandler(plugins.Plugin):
                         day = 1
                     else:
                         raise NotImplementedError(msg)
-                    bp = datetime.datetime(year=date.year, month=date.month, day=day,
-                        tzinfo=timezone.get_current_timezone()).date()
+                    bp = datetime.date(year=date.year, month=date.month, day=day)
                 elif self.billing_period == self.ANUAL:
                     if self.billing_point == self.ON_REGISTER:
                         month = order.registered_on.month
@@ -89,16 +88,24 @@ class ServiceHandler(plugins.Plugin):
                         year = bp.year - relativedelta.relativedelta(years=1)
                     if bp.month >= month:
                         year = bp.year + 1
-                    bp = datetime.datetime(year=year, month=month, day=day,
-                        tzinfo=timezone.get_current_timezone()).date()
+                    bp = datetime.date(year=year, month=month, day=day)
                 elif self.billing_period == self.NEVER:
                     bp = order.registered_on
                 else:
                     raise NotImplementedError(msg)
         if self.on_cancel != self.NOTHING and order.cancelled_on and order.cancelled_on < bp:
-            return order.cancelled_on
+            bp = order.cancelled_on
         return bp
     
+#    def aligned(self, date):
+#        if self.granularity == self.DAILY:
+#            return date
+#        elif self.granularity == self.MONTHLY:
+#            return datetime.date(year=date.year, month=date.month, day=1)
+#        elif self.granularity == self.ANUAL:
+#            return datetime.date(year=date.year, month=1, day=1)
+#        raise NotImplementedError
+    
     def get_price_size(self, ini, end):
         rdelta = relativedelta.relativedelta(end, ini)
         if self.billing_period == self.MONTHLY:
@@ -126,11 +133,9 @@ class ServiceHandler(plugins.Plugin):
         period = self.get_pricing_period()
         rdelta = self.get_pricing_rdelta()
         if period == self.MONTHLY:
-            ini = datetime.datetime(year=ini.year, month=ini.month, day=day,
-                                    tzinfo=timezone.get_current_timezone()).date()
+            ini = datetime.date(year=ini.year, month=ini.month, day=day)
         elif period == self.ANUAL:
-            ini = datetime.datetime(year=ini.year, month=month, day=day,
-                                    tzinfo=timezone.get_current_timezone()).date()
+            ini = datetime.date(year=ini.year, month=month, day=day)
         elif period == self.NEVER:
             yield ini, end
             raise StopIteration
@@ -246,12 +251,13 @@ class ServiceHandler(plugins.Plugin):
         for order in porders:
             bu = getattr(order, 'new_billed_until', order.billed_until)
             if bu:
-                if order.registered_on > ini and order.registered_on <= end:
+                registered = order.registered_on
+                if registered > ini and registered <= end:
                     counter += 1
-                if order.registered_on != bu and bu > ini and bu <= end:
+                if registered != bu and bu > ini and bu <= end:
                     counter += 1
                 if order.billed_until and order.billed_until != bu:
-                    if order.registered_on != order.billed_until and order.billed_until > ini and order.billed_until <= end:
+                    if registered != order.billed_until and order.billed_until > ini and order.billed_until <= end:
                         counter += 1
         return counter
     
@@ -331,6 +337,7 @@ class ServiceHandler(plugins.Plugin):
         #   In most cases:
         #       ini >= registered_date, end < registered_date
         # boundary lookup and exclude cancelled and billed
+        # TODO service.payment_style == self.POSTPAY  no discounts no shit on_cancel
         orders_ = []
         bp = None
         ini = datetime.date.max
@@ -339,7 +346,7 @@ class ServiceHandler(plugins.Plugin):
             cini = order.registered_on
             if order.billed_until:
                 # exclude cancelled and billed
-                if self.on_cancel != self.REFOUND:
+                if self.on_cancel != self.REFUND:
                     if order.cancelled_on and order.billed_until > order.cancelled_on:
                         continue
                 cini = order.billed_until
@@ -413,20 +420,22 @@ class ServiceHandler(plugins.Plugin):
                 order.new_billed_until = bp
                 if self.get_pricing_period() == self.NEVER:
                     # Changes (Mailbox disk-like)
-                    for ini, end, metric in order.get_metric(ini, bp, changes=True):
+                    for cini, cend, metric in order.get_metric(ini, bp, changes=True):
                         price = self.get_price(order, metric)
-                        lines.append(self.generate_line(order, price, ini, end, metric=metric))
-                else:
+                        lines.append(self.generate_line(order, price, cini, cend, metric=metric))
+                elif self.get_pricing_period() == self.billing_period:
                     # pricing_slots (Traffic-like)
-                    for ini, end in self.get_pricing_slots(ini, bp):
-                        metric = order.get_metric(ini, end)
+                    for cini, cend in self.get_pricing_slots(ini, bp):
+                        metric = order.get_metric(cini, cend)
                         price = self.get_price(order, metric)
-                        lines.append(self.generate_line(order, price, ini, end, metric=metric))
+                        lines.append(self.generate_line(order, price, cini, cend, metric=metric))
+                else:
+                    raise NotImplementedError
             else:
                 # One-time billing
                 if order.billed_until:
                     continue
-                date = order.registered_on
+                date = timezone.now().date()
                 order.new_billed_until = date
                 if self.get_pricing_period() == self.NEVER:
                     # get metric (Job-like)
diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py
index fef937ad..17b70f7f 100644
--- a/orchestra/apps/services/models.py
+++ b/orchestra/apps/services/models.py
@@ -77,6 +77,7 @@ autodiscover('handlers')
 
 class Service(models.Model):
     NEVER = ''
+#    DAILY = 'DAILY'
     MONTHLY = 'MONTHLY'
     ANUAL = 'ANUAL'
     TEN_DAYS = 'TEN_DAYS'
@@ -90,7 +91,7 @@ class Service(models.Model):
     NOTHING = 'NOTHING'
     DISCOUNT = 'DISCOUNT'
     COMPENSATE = 'COMPENSATE'
-    REFOUND = 'REFOUND'
+    REFUND = 'REFUND'
     PREPAY = 'PREPAY'
     POSTPAY = 'POSTPAY'
     STEP_PRICE = 'STEP_PRICE'
@@ -175,9 +176,27 @@ class Service(models.Model):
                 (NOTHING, _("Nothing")),
                 (DISCOUNT, _("Discount")),
                 (COMPENSATE, _("Compensat")),
-                (REFOUND, _("Refound")),
+                (REFUND, _("Refund")),
             ),
             default=DISCOUNT)
+#    on_broken_period = models.CharField(_("on broken period", max_length=16,
+#            help_text=_("Defines the billing behaviour when periods are incomplete on register and on cancel"),
+#            choices=(
+#                (NOTHING, _("Nothing, period is atomic")),
+#                (DISCOUNT, _("Bill partially")),
+#                (COMPENSATE, _("Compensate on cancel")),
+#                (REFUND, _("Refund on cancel")),
+#            ),
+#            default=DISCOUNT)
+#    granularity = models.CharField(_("granularity"), max_length=16,
+#            help_text=_("Defines the minimum size a period can be broken into"),
+#            choices=(
+#                (DAILY, _("One day")),
+#                (MONTHLY, _("One month")),
+#                (ANUAL, _("One year")),
+#            ),
+#            default=DAILY,
+#    )
     payment_style = models.CharField(_("payment style"), max_length=16,
             help_text=_("Designates whether this service should be paid after "
                         "consumtion (postpay/on demand) or prepaid"),
@@ -194,14 +213,14 @@ class Service(models.Model):
 #                (ONE_MONTH, _("One month")),
 #            ),
 #            default=NEVER)
-#    refound_period = models.CharField(_("refound period"), max_length=16,
-#            help_text=_("Period in which automatic refound will be performed on "
+#    refund_period = models.CharField(_("refund period"), max_length=16,
+#            help_text=_("Period in which automatic refund will be performed on "
 #                        "service cancellation"),
 #            choices=(
-#                (NEVER, _("Never refound")),
+#                (NEVER, _("Never refund")),
 #                (TEN_DAYS, _("Ten days")),
 #                (ONE_MONTH, _("One month")),
-#                (ALWAYS, _("Always refound")),
+#                (ALWAYS, _("Always refund")),
 #            ),
 #            default=NEVER, blank=True)