JobBillingTest tests passing

This commit is contained in:
Marc 2014-09-23 16:23:36 +00:00
parent 259bc07b71
commit c0e8e9f85d
6 changed files with 111 additions and 79 deletions

View File

@ -106,3 +106,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
@property @property
def register_on(self): def register_on(self):
return order.register_at.date() return order.register_at.date()
* latest by 'id' *always*

View File

@ -141,14 +141,15 @@ class Order(models.Model):
self.save() self.save()
logger.info("CANCELLED order id: {id}".format(id=self.id)) logger.info("CANCELLED order id: {id}".format(id=self.id))
def get_metric(self, ini, end, changes=False): def get_metric(self, *args, **kwargs):
if changes: if kwargs.pop('changes', False):
ini, end = args
result = [] result = []
prev = None prev = None
for metric in self.metrics.filter(created_on__lt=end).order_by('created_on'): for metric in self.metrics.filter(created_on__lt=end).order_by('id'):
created = metric.created_on.date() created = metric.created_on
if created > ini: if created > ini:
cini = prev.created_on.date() cini = prev.created_on
if not result: if not result:
cini = ini cini = ini
result.append((cini, created, prev.value)) result.append((cini, created, prev.value))
@ -156,8 +157,20 @@ class Order(models.Model):
if created < end: if created < end:
result.append((created, end, metric.value)) result.append((created, end, metric.value))
return result return result
try: if kwargs:
raise AttributeError
if len(args) == 2:
ini, end = args
metrics = self.metrics.filter(updated_on__lt=end, updated_on__gte=ini) 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)
elif not args:
return self.metrics.latest('updated_on').value
else:
raise AttributeError
try:
return metrics.latest('updated_on').value return metrics.latest('updated_on').value
except MetricStorage.DoesNotExist: except MetricStorage.DoesNotExist:
return decimal.Decimal(0) return decimal.Decimal(0)
@ -166,11 +179,11 @@ class Order(models.Model):
class MetricStorage(models.Model): class MetricStorage(models.Model):
order = models.ForeignKey(Order, verbose_name=_("order"), related_name='metrics') order = models.ForeignKey(Order, verbose_name=_("order"), related_name='metrics')
value = models.DecimalField(_("value"), max_digits=16, decimal_places=2) value = models.DecimalField(_("value"), max_digits=16, decimal_places=2)
created_on = models.DateTimeField(_("created"), auto_now_add=True) created_on = models.DateField(_("created"), auto_now_add=True)
updated_on = models.DateTimeField(_("updated")) updated_on = models.DateTimeField(_("updated"))
class Meta: class Meta:
get_latest_by = 'created_on' get_latest_by = 'id'
def __unicode__(self): def __unicode__(self):
return unicode(self.order) return unicode(self.order)

View File

@ -38,7 +38,7 @@ class FTPBillingTest(BaseBillingTest):
metric='', metric='',
pricing_period=Service.NEVER, pricing_period=Service.NEVER,
rate_algorithm=Service.STEP_PRICE, rate_algorithm=Service.STEP_PRICE,
on_cancel=Service.DISCOUNT, on_cancel=Service.COMPENSATE,
payment_style=Service.PREPAY, payment_style=Service.PREPAY,
tax=0, tax=0,
nominal_price=10, nominal_price=10,
@ -269,7 +269,6 @@ class TrafficBillingTest(BaseBillingTest):
return service return service
def create_traffic_resource(self): def create_traffic_resource(self):
from orchestra.apps.resources.models import Resource
self.resource = Resource.objects.create( self.resource = Resource.objects.create(
name='traffic', name='traffic',
content_type=ContentType.objects.get_for_model(Account), content_type=ContentType.objects.get_for_model(Account),
@ -341,7 +340,7 @@ class MailboxBillingTest(BaseBillingTest):
metric='', metric='',
pricing_period=Service.NEVER, pricing_period=Service.NEVER,
rate_algorithm=Service.STEP_PRICE, rate_algorithm=Service.STEP_PRICE,
on_cancel=Service.DISCOUNT, on_cancel=Service.COMPENSATE,
payment_style=Service.PREPAY, payment_style=Service.PREPAY,
tax=0, tax=0,
nominal_price=10 nominal_price=10
@ -467,15 +466,15 @@ class JobBillingTest(BaseBillingTest):
is_fee=False, is_fee=False,
metric='miscellaneous.amount', metric='miscellaneous.amount',
pricing_period=Service.BILLING_PERIOD, pricing_period=Service.BILLING_PERIOD,
rate_algorithm=Service.STEP_PRICE, rate_algorithm=Service.MATCH_PRICE,
on_cancel=Service.NOTHING, on_cancel=Service.NOTHING,
payment_style=Service.POSTPAY, payment_style=Service.POSTPAY,
tax=0, tax=0,
nominal_price=10 nominal_price=20
) )
plan = Plan.objects.create(is_default=True, name='Default') plan = Plan.objects.create(is_default=True, name='Default')
service.rates.create(plan=plan, quantity=1, price=0) service.rates.create(plan=plan, quantity=1, price=20)
service.rates.create(plan=plan, quantity=11, price=10) service.rates.create(plan=plan, quantity=10, price=15)
return service return service
def create_job(self, amount, account=None): def create_job(self, amount, account=None):
@ -488,9 +487,14 @@ class JobBillingTest(BaseBillingTest):
def test_job(self): def test_job(self):
service = self.create_job_service() service = self.create_job_service()
account = self.create_account() account = self.create_account()
job = self.create_job(10, account=account)
print service.orders.all() job = self.create_job(5, account=account)
print service.orders.bill()[0].get_total() bill = service.orders.bill()[0]
self.assertEqual(5*20, bill.get_total())
job = self.create_job(100, account=account)
bill = service.orders.bill(new_open=True)[0]
self.assertEqual(100*15, bill.get_total())
class PlanBillingTest(BaseBillingTest): class PlanBillingTest(BaseBillingTest):

View File

@ -124,14 +124,13 @@ class ServiceHandler(plugins.Plugin):
day = ini.day day = ini.day
month = ini.month month = ini.month
period = self.get_pricing_period() period = self.get_pricing_period()
rdelta = self.get_pricing_rdelta()
if period == self.MONTHLY: if period == self.MONTHLY:
ini = datetime.datetime(year=ini.year, month=ini.month, day=day, ini = datetime.datetime(year=ini.year, month=ini.month, day=day,
tzinfo=timezone.get_current_timezone()).date() tzinfo=timezone.get_current_timezone()).date()
rdelta = relativedelta.relativedelta(months=1)
elif period == self.ANUAL: elif period == self.ANUAL:
ini = datetime.datetime(year=ini.year, month=month, day=day, ini = datetime.datetime(year=ini.year, month=month, day=day,
tzinfo=timezone.get_current_timezone()).date() tzinfo=timezone.get_current_timezone()).date()
rdelta = relativedelta.relativedelta(years=1)
elif period == self.NEVER: elif period == self.NEVER:
yield ini, end yield ini, end
raise StopIteration raise StopIteration
@ -144,6 +143,15 @@ class ServiceHandler(plugins.Plugin):
break break
ini = next ini = next
def get_pricing_rdelta(self):
period = self.get_pricing_period()
if period == self.MONTHLY:
return relativedelta.relativedelta(months=1)
elif period == self.ANUAL:
return relativedelta.relativedelta(years=1)
elif period == self.NEVER:
return None
def generate_discount(self, line, dtype, price): def generate_discount(self, line, dtype, price):
line.discounts.append(AttributeDict(**{ line.discounts.append(AttributeDict(**{
'type': dtype, 'type': dtype,
@ -162,6 +170,7 @@ class ServiceHandler(plugins.Plugin):
computed = kwargs.pop('computed', False) computed = kwargs.pop('computed', False)
if kwargs: if kwargs:
raise AttributeError raise AttributeError
size = self.get_price_size(ini, end) size = self.get_price_size(ini, end)
if not computed: if not computed:
price = price * size price = price * size
@ -184,7 +193,7 @@ class ServiceHandler(plugins.Plugin):
self.generate_discount(line, 'volume', price-subtotal) self.generate_discount(line, 'volume', price-subtotal)
return line return line
def assign_compensations(self, givers, receivers, commit=True): def assign_compensations(self, givers, receivers, **options):
compensations = [] compensations = []
for order in givers: for order in givers:
if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until: if order.billed_until and order.cancelled_on and order.cancelled_on < order.billed_until:
@ -202,7 +211,7 @@ class ServiceHandler(plugins.Plugin):
# TODO get min right # TODO get min right
comp.order.new_billed_until = min(comp.order.billed_until, comp.ini, comp.order.new_billed_until = min(comp.order.billed_until, comp.ini,
getattr(comp.order, 'new_billed_until', datetime.date.max)) getattr(comp.order, 'new_billed_until', datetime.date.max))
if commit: if options.get('commit', True):
for order in givers: for order in givers:
if hasattr(order, 'new_billed_until'): if hasattr(order, 'new_billed_until'):
order.billed_until = order.new_billed_until order.billed_until = order.new_billed_until
@ -246,7 +255,7 @@ class ServiceHandler(plugins.Plugin):
counter += 1 counter += 1
return counter return counter
def bill_concurrent_orders(self, account, porders, rates, ini, end, commit=True): def bill_concurrent_orders(self, account, porders, rates, ini, end):
# Concurrent # Concurrent
# Get pricing orders # Get pricing orders
priced = {} priced = {}
@ -286,21 +295,14 @@ class ServiceHandler(plugins.Plugin):
order.new_billed_until = new_end order.new_billed_until = new_end
line = self.generate_line(order, price, ini, new_end or end, discounts=discounts, computed=True) line = self.generate_line(order, price, ini, new_end or end, discounts=discounts, computed=True)
lines.append(line) lines.append(line)
if commit:
order.billed_until = order.new_billed_until
order.save()
return lines return lines
def bill_registered_or_renew_events(self, account, porders, rates, commit=True): def bill_registered_or_renew_events(self, account, porders, rates):
# Before registration # Before registration
lines = [] lines = []
period = self.get_pricing_period() rdelta = self.get_pricing_rdelta()
if period == self.MONTHLY: if not rdelta:
rdelta = relativedelta.relativedelta(months=1) raise NotImplementedError
elif period == self.ANUAL:
rdelta = relativedelta.relativedelta(years=1)
elif period == self.NEVER:
raise NotImplementedError("Rates with no pricing period?")
for position, order in enumerate(porders, start=1): for position, order in enumerate(porders, start=1):
if hasattr(order, 'new_billed_until'): if hasattr(order, 'new_billed_until'):
pend = order.billed_until or order.registered_on pend = order.billed_until or order.registered_on
@ -319,9 +321,6 @@ class ServiceHandler(plugins.Plugin):
size = self.get_price_size(ini, end) size = self.get_price_size(ini, end)
line = self.generate_line(order, price, ini, end, discounts=discounts) line = self.generate_line(order, price, ini, end, discounts=discounts)
lines.append(line) lines.append(line)
if commit:
order.billed_until = order.new_billed_until
order.save()
return lines return lines
def bill_with_orders(self, orders, account, **options): def bill_with_orders(self, orders, account, **options):
@ -329,14 +328,11 @@ class ServiceHandler(plugins.Plugin):
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0) # date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
# In most cases: # In most cases:
# ini >= registered_date, end < registered_date # ini >= registered_date, end < registered_date
commit = options.get('commit', True)
# boundary lookup and exclude cancelled and billed # boundary lookup and exclude cancelled and billed
orders_ = [] orders_ = []
bp = None bp = None
ini = datetime.date.max ini = datetime.date.max
end = datetime.date.min end = datetime.date.min
# TODO compensation with one time billing?
for order in orders: for order in orders:
cini = order.registered_on cini = order.registered_on
if order.billed_until: if order.billed_until:
@ -354,24 +350,33 @@ class ServiceHandler(plugins.Plugin):
# Compensation # Compensation
related_orders = account.orders.filter(service=self.service) related_orders = account.orders.filter(service=self.service)
if self.on_cancel == self.DISCOUNT: if self.on_cancel == self.COMPENSATE:
# Get orders pending for compensation # Get orders pending for compensation
givers = list(related_orders.givers(ini, end)) givers = list(related_orders.givers(ini, end))
givers.sort(cmp=helpers.cmp_billed_until_or_registered_on) givers.sort(cmp=helpers.cmp_billed_until_or_registered_on)
orders.sort(cmp=helpers.cmp_billed_until_or_registered_on) orders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
self.assign_compensations(givers, orders, commit=commit) self.assign_compensations(givers, orders, **options)
rates = self.get_rates(account) rates = self.get_rates(account)
if rates: has_billing_period = self.billing_period != self.NEVER
has_pricing_period = self.get_pricing_period() != self.NEVER
if rates and (has_billing_period or has_pricing_period):
concurrent = has_billing_period and not has_pricing_period
if not concurrent:
rdelta = self.get_pricing_rdelta()
ini -= rdelta
porders = related_orders.pricing_orders(ini, end) porders = related_orders.pricing_orders(ini, end)
porders = list(set(orders).union(set(porders))) porders = list(set(orders).union(set(porders)))
porders.sort(cmp=helpers.cmp_billed_until_or_registered_on) porders.sort(cmp=helpers.cmp_billed_until_or_registered_on)
if self.billing_period != self.NEVER and self.get_pricing_period() == self.NEVER: if concurrent:
lines = self.bill_concurrent_orders(account, porders, rates, ini, end, commit=commit) # Periodic billing with no pricing period
lines = self.bill_concurrent_orders(account, porders, rates, ini, end)
else: else:
# TODO compensation in this case? # TODO compensation in this case?
lines = self.bill_registered_or_renew_events(account, porders, rates, commit=commit) # Periodic and one-time billing with pricing period
lines = self.bill_registered_or_renew_events(account, porders, rates)
else: else:
# No rates optimization or one-time billing without pricing period
lines = [] lines = []
price = self.nominal_price price = self.nominal_price
# Calculate nominal price # Calculate nominal price
@ -387,47 +392,45 @@ class ServiceHandler(plugins.Plugin):
end = new_end end = new_end
line = self.generate_line(order, price, ini, end, discounts=discounts) line = self.generate_line(order, price, ini, end, discounts=discounts)
lines.append(line) lines.append(line)
if commit:
order.billed_until = order.new_billed_until
order.save()
return lines return lines
def bill_with_metric(self, orders, account, **options): def bill_with_metric(self, orders, account, **options):
lines = [] lines = []
commit = options.get('commit', True)
bp = None bp = None
for order in orders: for order in orders:
if order.billed_until and order.cancelled_on >= order.billed_until: if order.billed_until and order.cancelled_on and order.cancelled_on >= order.billed_until:
continue continue
bp = self.get_billing_point(order, bp=bp, **options)
ini = order.billed_until or order.registered_on
if bp <= ini:
# TODO except one time service
continue
order.new_billed_until = bp
if self.billing_period != self.NEVER: if self.billing_period != self.NEVER:
bp = self.get_billing_point(order, bp=bp, **options)
ini = order.billed_until or order.registered_on
# Periodic billing
if bp <= ini:
continue
order.new_billed_until = bp
if self.get_pricing_period() == self.NEVER: if self.get_pricing_period() == self.NEVER:
# Changes # Changes (Mailbox disk-like)
for ini, end, metric in order.get_metric(ini, bp, changes=True): for ini, end, metric in order.get_metric(ini, bp, changes=True):
price = self.get_price(order, metric) price = self.get_price(order, metric)
lines.append(self.generate_line(order, price, ini, end, metric=metric)) lines.append(self.generate_line(order, price, ini, end, metric=metric))
else: else:
# pricing_slots # pricing_slots (Traffic-like)
for ini, end in self.get_pricing_slots(ini, bp): for ini, end in self.get_pricing_slots(ini, bp):
metric = order.get_metric(ini, end) metric = order.get_metric(ini, end)
price = self.get_price(order, metric) price = self.get_price(order, metric)
lines.append(self.generate_line(order, price, ini, end, metric=metric)) lines.append(self.generate_line(order, price, ini, end, metric=metric))
else: else:
# One-time billing
if order.billed_until:
continue
date = order.registered_on
order.new_billed_until = date
if self.get_pricing_period() == self.NEVER: if self.get_pricing_period() == self.NEVER:
# get metric # get metric (Job-like)
metric = order.get_metric(ini, end) metric = order.get_metric(date)
price = self.get_price(order, metric) price = self.get_price(order, metric)
lines.append(self.generate_line(order, price, ini, bp, metric=metric)) lines.append(self.generate_line(order, price, date, metric=metric))
else: else:
raise NotImplementedError raise NotImplementedError
if commit:
order.billed_until = order.new_billed_until
order.save()
return lines return lines
def generate_bill_lines(self, orders, account, **options): def generate_bill_lines(self, orders, account, **options):
@ -437,4 +440,8 @@ class ServiceHandler(plugins.Plugin):
lines = self.bill_with_orders(orders, account, **options) lines = self.bill_with_orders(orders, account, **options)
else: else:
lines = self.bill_with_metric(orders, account, **options) lines = self.bill_with_metric(orders, account, **options)
if options.get('commit', True):
for line in lines:
line.order.billed_until = line.order.new_billed_until
line.order.save()
return lines return lines

View File

@ -89,6 +89,7 @@ class Service(models.Model):
CONCURRENT = 'CONCURRENT' CONCURRENT = 'CONCURRENT'
NOTHING = 'NOTHING' NOTHING = 'NOTHING'
DISCOUNT = 'DISCOUNT' DISCOUNT = 'DISCOUNT'
COMPENSATE = 'COMPENSATE'
REFOUND = 'REFOUND' REFOUND = 'REFOUND'
PREPAY = 'PREPAY' PREPAY = 'PREPAY'
POSTPAY = 'POSTPAY' POSTPAY = 'POSTPAY'
@ -173,6 +174,7 @@ class Service(models.Model):
choices=( choices=(
(NOTHING, _("Nothing")), (NOTHING, _("Nothing")),
(DISCOUNT, _("Discount")), (DISCOUNT, _("Discount")),
(COMPENSATE, _("Compensat")),
(REFOUND, _("Refound")), (REFOUND, _("Refound")),
), ),
default=DISCOUNT) default=DISCOUNT)
@ -266,13 +268,13 @@ class Service(models.Model):
""" """
if rates is None: if rates is None:
rates = self.get_rates(account) rates = self.get_rates(account)
if rates:
rates = self.rate_method(rates, metric)
if not rates: if not rates:
rates = [{ rates = [{
'quantity': metric, 'quantity': metric,
'price': self.nominal_price, 'price': self.nominal_price,
}] }]
else:
rates = self.rate_method(rates, metric)
counter = 0 counter = 0
if position is None: if position is None:
ant_counter = 0 ant_counter = 0

View File

@ -44,7 +44,6 @@ def _compute(rates, metric):
def step_price(rates, metric): def step_price(rates, metric):
# Step price # Step price
# TODO allow multiple plans
group = [] group = []
minimal = (sys.maxint, []) minimal = (sys.maxint, [])
for plan, rates in rates.group_by('plan').iteritems(): for plan, rates in rates.group_by('plan').iteritems():
@ -104,18 +103,22 @@ def match_price(rates, metric):
selected = False selected = False
prev = None prev = None
for rate in rates.distinct(): for rate in rates.distinct():
if prev and prev.plan != rate.plan: if prev:
if not selected and prev.quantity <= metric: if prev.plan != rate.plan:
candidates.append(prev) if not selected and prev.quantity <= metric:
selected = False candidates.append(prev)
if not selected and rate.quantity > metric: selected = False
candidates.append(prev) if not selected and rate.quantity > metric:
selected = True if prev.quantity <= metric:
candidates.append(prev)
selected = True
prev = rate prev = rate
if not selected and prev.quantity <= metric: if not selected and prev.quantity <= metric:
candidates.append(prev) candidates.append(prev)
candidates.sort(key=lambda r: r.price) candidates.sort(key=lambda r: r.price)
return [AttributeDict(**{ if candidates:
'quantity': metric, return [AttributeDict(**{
'price': candidates[0].price, 'quantity': metric,
})] 'price': candidates[0].price,
})]
return None