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
def register_on(self):
return order.register_at.date()
* latest by 'id' *always*

View file

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

View file

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

View file

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

View file

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

View file

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