Enough of fucking billing tests0

This commit is contained in:
Marc 2014-09-26 10:38:50 +00:00
parent c8651cf4ed
commit af323ebe25
15 changed files with 150 additions and 51 deletions

View File

@ -100,9 +100,13 @@ class Order(models.Model):
return str(self.service) return str(self.service)
@classmethod @classmethod
def update_orders(cls, instance): def update_orders(cls, instance, service=None):
Service = get_model(*settings.ORDERS_SERVICE_MODEL.split('.')) if service is None:
for service in Service.get_services(instance): Service = get_model(*settings.ORDERS_SERVICE_MODEL.split('.'))
services = Service.get_services(instance)
else:
services = [service]
for service in services:
orders = Order.objects.by_object(instance, service=service).active() orders = Order.objects.by_object(instance, service=service).active()
if service.handler.matches(instance): if service.handler.matches(instance):
if not orders: if not orders:

View File

@ -101,6 +101,7 @@ class Resource(models.Model):
task.crontab = self.crontab task.crontab = self.crontab
task.save() task.save()
if created: if created:
# This only work on tests because of multiprocessing used on real deployments
create_resource_relation() create_resource_relation()
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):

View File

@ -0,0 +1,14 @@
from django.contrib import messages
from django.db import transaction
from django.utils.translation import ugettext_lazy as _
@transaction.atomic
def update_orders(modeladmin, request, queryset):
for service in queryset:
service.update_orders()
modeladmin.log_change(request, transaction, 'Update orders')
msg = _("Orders for %s selected services have been updated.") % queryset.count()
modeladmin.message_user(request, msg)
update_orders.url_name = 'update-orders'
update_orders.verbose_name = _("Update orders")

View File

@ -4,10 +4,12 @@ from django.core.urlresolvers import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from orchestra.admin import ChangeViewActionsMixin
from orchestra.admin.filters import UsedContentTypeFilter from orchestra.admin.filters import UsedContentTypeFilter
from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.core import services from orchestra.core import services
from .actions import update_orders
from .models import Plan, ContractedPlan, Rate, Service from .models import Plan, ContractedPlan, Rate, Service
@ -27,7 +29,7 @@ class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin):
list_filter = ('plan__name',) list_filter = ('plan__name',)
class ServiceAdmin(admin.ModelAdmin): class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin):
list_display = ( list_display = (
'description', 'content_type', 'handler_type', 'num_orders', 'is_active' 'description', 'content_type', 'handler_type', 'num_orders', 'is_active'
) )
@ -49,6 +51,8 @@ class ServiceAdmin(admin.ModelAdmin):
}), }),
) )
inlines = [RateInline] inlines = [RateInline]
actions = [update_orders]
change_view_actions = actions
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
""" Improve performance of account field and filter by account """ """ Improve performance of account field and filter by account """

View File

@ -18,6 +18,8 @@ class ServiceHandler(plugins.Plugin):
""" """
Separates all the logic of billing handling from the model allowing to better Separates all the logic of billing handling from the model allowing to better
customize its behaviout customize its behaviout
Relax and enjoy the journey.
""" """
model = None model = None
@ -213,7 +215,6 @@ class ServiceHandler(plugins.Plugin):
compensations, used_compensations = helpers.compensate(interval, compensations) compensations, used_compensations = helpers.compensate(interval, compensations)
order._compensations = used_compensations order._compensations = used_compensations
for comp in used_compensations: for comp in used_compensations:
# 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 options.get('commit', True): if options.get('commit', True):
@ -337,7 +338,6 @@ class ServiceHandler(plugins.Plugin):
# In most cases: # In most cases:
# ini >= registered_date, end < registered_date # ini >= registered_date, end < registered_date
# boundary lookup and exclude cancelled and billed # boundary lookup and exclude cancelled and billed
# TODO service.payment_style == self.POSTPAY no discounts no shit on_cancel
orders_ = [] orders_ = []
bp = None bp = None
ini = datetime.date.max ini = datetime.date.max
@ -359,7 +359,7 @@ 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.COMPENSATE: if self.payment_style == self.PREPAY and 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)
@ -381,7 +381,6 @@ class ServiceHandler(plugins.Plugin):
# Periodic billing with no pricing period # Periodic billing with no pricing period
lines = self.bill_concurrent_orders(account, porders, rates, ini, end) lines = self.bill_concurrent_orders(account, porders, rates, ini, end)
else: else:
# TODO compensation in this case?
# Periodic and one-time billing with pricing period # Periodic and one-time billing with pricing period
lines = self.bill_registered_or_renew_events(account, porders, rates) lines = self.bill_registered_or_renew_events(account, porders, rates)
else: else:

View File

@ -3,6 +3,7 @@ import sys
from django.db import models from django.db import models
from django.db.models import F, Q from django.db.models import F, Q
from django.db.models.loading import get_model
from django.db.models.signals import pre_delete, post_delete, post_save from django.db.models.signals import pre_delete, post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.contenttypes import generic from django.contrib.contenttypes import generic
@ -324,6 +325,12 @@ class Service(models.Model):
@property @property
def rate_method(self): def rate_method(self):
return self.RATE_METHODS[self.rate_algorithm] return self.RATE_METHODS[self.rate_algorithm]
def update_orders(self):
order_model = get_model(settings.SERVICES_ORDER_MODEL)
related_model = self.content_type.model_class()
for instance in related_model.objects.all():
order_model.update_orders(instance, service=self)
accounts.register(ContractedPlan) accounts.register(ContractedPlan)

View File

@ -42,11 +42,32 @@ def _compute(rates, metric):
return value, steps return value, steps
def _prepend_missing(rates):
"""
Support for incomplete rates
When first rate (quantity=5, price=10) defaults to nominal_price
"""
if rates:
first = rates[0]
if first.quantity == 0:
first.quantity = 1
elif first.quantity > 1:
if not isinstance(rates, list):
rates = list(rates)
service = first.service
rate_class = type(first)
rates.insert(0,
rate_class(service=service, plan=first.plan, quantity=1, price=service.nominal_price)
)
return rates
def step_price(rates, metric): def step_price(rates, metric):
# Step price # Step price
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():
rates = _prepend_missing(rates)
value, steps = _compute(rates, metric) value, steps = _compute(rates, metric)
if plan.is_combinable: if plan.is_combinable:
group.append(steps) group.append(steps)
@ -102,7 +123,8 @@ def match_price(rates, metric):
candidates = [] candidates = []
selected = False selected = False
prev = None prev = None
for rate in rates.distinct(): rates = _prepend_missing(rates.distinct())
for rate in rates:
if prev: if prev:
if prev.plan != rate.plan: if prev.plan != rate.plan:
if not selected and prev.quantity <= metric: if not selected and prev.quantity <= metric:

View File

@ -12,3 +12,6 @@ SERVICES_SERVICE_DEFAUL_TAX = getattr(settings, 'ORDERS_SERVICE_DFAULT_TAX', 0)
SERVICES_SERVICE_ANUAL_BILLING_MONTH = getattr(settings, 'SERVICES_SERVICE_ANUAL_BILLING_MONTH', 4) SERVICES_SERVICE_ANUAL_BILLING_MONTH = getattr(settings, 'SERVICES_SERVICE_ANUAL_BILLING_MONTH', 4)
SERVICES_ORDER_MODEL = getattr(settings, 'SERVICES_ORDER_MODEL', 'orders.Order')

View File

@ -36,8 +36,9 @@ class DomainBillingTest(BaseBillingTest):
if not account: if not account:
account = self.create_account() account = self.create_account()
domain_name = '%s.es' % random_ascii(10) domain_name = '%s.es' % random_ascii(10)
domain_service, __ = MiscService.objects.get_or_create(name='domain .es', description='Domain .ES') domain_service, __ = MiscService.objects.get_or_create(name='domain .es',
return Miscellaneous.objects.create(service=domain_service, description=domain_name, account=account) description='Domain .ES')
return account.miscellaneous.create(service=domain_service, description=domain_name)
def test_domain(self): def test_domain(self):
service = self.create_domain_service() service = self.create_domain_service()

View File

@ -98,6 +98,3 @@ class FTPBillingTest(BaseBillingTest):
order = account.orders.order_by('-id').first() order = account.orders.order_by('-id').first()
self.assertEqual(first_bp, order.billed_until) self.assertEqual(first_bp, order.billed_until)
self.assertEqual(decimal.Decimal(0), bills[0].get_total()) self.assertEqual(decimal.Decimal(0), bills[0].get_total())
def test_ftp_account_with_rates(self):
pass

View File

@ -34,8 +34,9 @@ class JobBillingTest(BaseBillingTest):
if not account: if not account:
account = self.create_account() account = self.create_account()
description = 'Random Job %s' % random_ascii(10) description = 'Random Job %s' % random_ascii(10)
service, __ = MiscService.objects.get_or_create(name='job', description=description, has_amount=True) service, __ = MiscService.objects.get_or_create(name='job', description=description,
return Miscellaneous.objects.create(service=service, description=description, account=account, amount=amount) has_amount=True)
return account.miscellaneous.create(service=service, description=description, amount=amount)
def test_job(self): def test_job(self):
service = self.create_job_service() service = self.create_job_service()
@ -48,5 +49,3 @@ class JobBillingTest(BaseBillingTest):
job = self.create_job(100, account=account) job = self.create_job(100, account=account)
bill = account.orders.bill(new_open=True)[0] bill = account.orders.bill(new_open=True)[0]
self.assertEqual(100*15, bill.get_total()) self.assertEqual(100*15, bill.get_total())

View File

@ -156,4 +156,3 @@ class MailboxBillingTest(BaseBillingTest):
with freeze_time(now+relativedelta(months=6)): with freeze_time(now+relativedelta(months=6)):
bills = service.orders.bill(new_open=True, **options) bills = service.orders.bill(new_open=True, **options)
self.assertEqual([], bills) self.assertEqual([], bills)

View File

@ -1,8 +1,6 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from orchestra.apps.miscellaneous.models import MiscService, Miscellaneous from ...models import Service, Plan, ContractedPlan
from ...models import Service, Plan
from . import BaseBillingTest from . import BaseBillingTest
@ -11,8 +9,8 @@ class PlanBillingTest(BaseBillingTest):
def create_plan_service(self): def create_plan_service(self):
service = Service.objects.create( service = Service.objects.create(
description="Association membership fee", description="Association membership fee",
content_type=ContentType.objects.get_for_model(Miscellaneous), content_type=ContentType.objects.get_for_model(ContractedPlan),
match="account.is_active and account.type == 'ASSOCIATION'", match="contractedplan.plan.name == 'association_fee'",
billing_period=Service.ANUAL, billing_period=Service.ANUAL,
billing_point=Service.FIXED_DATE, billing_point=Service.FIXED_DATE,
is_fee=True, is_fee=True,
@ -26,12 +24,28 @@ class PlanBillingTest(BaseBillingTest):
) )
return service return service
def create_plan(self): def create_plan(self, account=None):
if not account: if not account:
account = self.create_account() account = self.create_account()
domain_name = '%s.es' % random_ascii(10) plan, __ = Plan.objects.get_or_create(name='association_fee')
domain_service, __ = MiscService.objects.get_or_create(name='domain .es', description='Domain .ES') return plan.contracts.create(account=account)
return Miscellaneous.objects.create(service=domain_service, description=domain_name, account=account)
def test_update_orders(self):
account = self.create_account()
account1 = self.create_account()
self.create_plan(account=account)
self.create_plan(account=account1)
service = self.create_plan_service()
self.assertEqual(0, service.orders.count())
service.update_orders()
self.assertEqual(2, service.orders.count())
def test_plan(self): def test_plan(self):
pass account = self.create_account()
service = self.create_plan_service()
self.create_plan(account=account)
bill = account.orders.bill().pop()
self.assertEqual(bill.FEE, bill.type)
# TODO test price with multiple plans

View File

@ -54,7 +54,8 @@ class BaseTrafficBillingTest(BaseBillingTest):
def report_traffic(self, account, value): def report_traffic(self, account, value):
ct = ContentType.objects.get_for_model(Account) ct = ContentType.objects.get_for_model(Account)
object_id = account.pk object_id = account.pk
MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user, value=value, date=timezone.now()) MonitorData.objects.create(monitor='FTPTraffic', content_object=account.user,
value=value, date=timezone.now())
data = ResourceData.get_or_create(account, self.resource) data = ResourceData.get_or_create(account, self.resource)
data.update() data.update()
@ -85,11 +86,20 @@ class TrafficBillingTest(BaseTrafficBillingTest):
resource = self.create_traffic_resource() resource = self.create_traffic_resource()
account1 = self.create_account() account1 = self.create_account()
account2 = self.create_account() account2 = self.create_account()
# TODO self.report_traffic(account1, 10**10)
self.report_traffic(account2, 10**10*5)
with freeze_time(timezone.now()+relativedelta(months=1)):
bill1 = account1.orders.bill().pop()
bill2 = account2.orders.bill().pop()
self.assertNotEqual(bill1.get_total(), bill2.get_total())
class TrafficPrepayBillingTest(BaseTrafficBillingTest): class TrafficPrepayBillingTest(BaseTrafficBillingTest):
METRIC = "max((account.resources.traffic.used or 0) - getattr(account.miscellaneous.filter(is_active=True, 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): def create_prepay_service(self):
service = Service.objects.create( service = Service.objects.create(
@ -114,8 +124,9 @@ class TrafficPrepayBillingTest(BaseTrafficBillingTest):
if not account: if not account:
account = self.create_account() account = self.create_account()
name = 'traffic prepay' name = 'traffic prepay'
service, __ = MiscService.objects.get_or_create(name='traffic prepay', description='Traffic prepay', has_amount=True) service, __ = MiscService.objects.get_or_create(name='traffic prepay',
return Miscellaneous.objects.create(service=service, description=name, account=account, amount=amount) description='Traffic prepay', has_amount=True)
return account.miscellaneous.create(service=service, description=name, amount=amount)
def test_traffic_prepay(self): def test_traffic_prepay(self):
service = self.create_traffic_service() service = self.create_traffic_service()

View File

@ -313,6 +313,47 @@ class HandlerTests(BaseTestCase):
self.assertEqual(decimal.Decimal('9.00'), results[0].price) self.assertEqual(decimal.Decimal('9.00'), results[0].price)
self.assertEqual(30, results[0].quantity) self.assertEqual(30, results[0].quantity)
def test_incomplete_rates(self):
service = self.create_ftp_service()
account = self.create_account()
superplan = Plan.objects.create(
name='SUPER', allow_multiple=False, is_combinable=True)
service.rates.create(plan=superplan, quantity=4, price=9)
service.rates.create(plan=superplan, quantity=10, price=1)
account.plans.create(plan=superplan)
results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30)
rates = [
{'price': decimal.Decimal('10.00'), 'quantity': 3},
{'price': decimal.Decimal('9.00'), 'quantity': 6},
{'price': decimal.Decimal('1.00'), 'quantity': 21}
]
for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price)
self.assertEqual(rate['quantity'], result.quantity)
def test_zero_rates(self):
service = self.create_ftp_service()
account = self.create_account()
superplan = Plan.objects.create(
name='SUPER', allow_multiple=False, is_combinable=True)
service.rates.create(plan=superplan, quantity=0, price=0)
service.rates.create(plan=superplan, quantity=3, price=10)
service.rates.create(plan=superplan, quantity=4, price=9)
service.rates.create(plan=superplan, quantity=10, price=1)
account.plans.create(plan=superplan)
results = service.get_rates(account, cache=False)
results = service.rate_method(results, 30)
rates = [
{'price': decimal.Decimal('0.00'), 'quantity': 2},
{'price': decimal.Decimal('10.00'), 'quantity': 1},
{'price': decimal.Decimal('9.00'), 'quantity': 6},
{'price': decimal.Decimal('1.00'), 'quantity': 21}
]
for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price)
self.assertEqual(rate['quantity'], result.quantity)
def test_rates_allow_multiple(self): def test_rates_allow_multiple(self):
service = self.create_ftp_service() service = self.create_ftp_service()
account = self.create_account() account = self.create_account()
@ -352,20 +393,3 @@ class HandlerTests(BaseTestCase):
for rate, result in zip(rates, results): for rate, result in zip(rates, results):
self.assertEqual(rate['price'], result.price) self.assertEqual(rate['price'], result.price)
self.assertEqual(rate['quantity'], result.quantity) self.assertEqual(rate['quantity'], result.quantity)
def test_generate_bill_lines_with_compensation(self):
service = self.create_ftp_service()
account = self.create_account()
now = timezone.now().date()
order = Order(
cancelled_on=now,
billed_until=now+relativedelta.relativedelta(years=2)
)
order1 = Order()
orders = [order, order1]
lines = service.handler.generate_bill_lines(orders, account, commit=False)
print lines
print len(lines)
# TODO
# TODO test incomplete rate 1 -> nominal_price 10 -> rate