JobBillingTest tests passing
This commit is contained in:
parent
259bc07b71
commit
c0e8e9f85d
3
TODO.md
3
TODO.md
|
@ -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*
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue