Handle edge cases of last day of the month of billing period.

This commit is contained in:
Santiago L 2021-03-31 12:11:53 +02:00
parent dc722ec17a
commit 2b06652a5b
1 changed files with 46 additions and 38 deletions

View File

@ -21,29 +21,29 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
"""
Separates all the logic of billing handling from the model allowing to better
customize its behaviout
Relax and enjoy the journey.
"""
_PLAN = 'plan'
_COMPENSATION = 'compensation'
_PREPAY = 'prepay'
model = None
def __init__(self, service):
self.service = service
def __getattr__(self, attr):
return getattr(self.service, attr)
@classmethod
def get_choices(cls):
choices = super(ServiceHandler, cls).get_choices()
return [('', _("Default"))] + choices
def validate_content_type(self, service):
pass
def validate_expression(self, service, method):
try:
obj = service.content_type.model_class().objects.all()[0]
@ -53,24 +53,24 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
bool(getattr(self, method)(obj))
except Exception as exc:
raise ValidationError(format_exception(exc))
def validate_match(self, service):
if not service.match:
service.match = 'True'
self.validate_expression(service, 'matches')
def validate_metric(self, service):
self.validate_expression(service, 'get_metric')
def validate_order_description(self, service):
self.validate_expression(service, 'get_order_description')
def get_content_type(self):
if not self.model:
return self.content_type
app_label, model = self.model.split('.')
return ContentType.objects.get_by_natural_key(app_label, model.lower())
def get_expression_context(self, instance):
return {
'instance': instance,
@ -85,14 +85,14 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
'log10': math.log10,
'Decimal': decimal.Decimal,
}
def matches(self, instance):
if not self.match:
# Blank expressions always evaluate True
return True
safe_locals = self.get_expression_context(instance)
return eval(self.match, safe_locals)
def get_ignore_delta(self):
if self.ignore_period == self.NEVER:
return None
@ -104,14 +104,14 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
return datetime.timedelta(months=value)
else:
raise ValueError("Unknown unit %s" % unit)
def get_order_ignore(self, order):
""" service trial delta """
ignore_delta = self.get_ignore_delta()
if ignore_delta and (order.cancelled_on-ignore_delta).date() <= order.registered_on:
return True
return order.ignore
def get_ignore(self, instance):
if self.ignore_superusers:
account = getattr(instance, 'account', instance)
@ -120,7 +120,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
if 'superuser' in settings.SERVICES_IGNORE_ACCOUNT_TYPE and account.is_superuser:
return True
return False
def get_metric(self, instance):
if self.metric:
safe_locals = self.get_expression_context(instance)
@ -128,7 +128,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
return eval(self.metric, safe_locals)
except Exception as exc:
raise type(exc)("'%s' evaluating metric for '%s' service" % (exc, self.service))
def get_order_description(self, instance):
safe_locals = self.get_expression_context(instance)
account = getattr(instance, 'account', instance)
@ -136,7 +136,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
if not self.order_description:
return '%s: %s' % (ugettext(self.description), instance)
return eval(self.order_description, safe_locals)
def get_billing_point(self, order, bp=None, **options):
cachable = bool(self.billing_point == self.FIXED_DATE and not options.get('fixed_point'))
if not cachable or bp is None:
@ -151,7 +151,10 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
else:
date = timezone.now().date()
if self.billing_point == self.ON_REGISTER:
day = order.registered_on.day
# handle edge cases of last day of the month:
# e.g. on March is 31 but on April 30
last_day_of_month = calendar.monthrange(date.year, date.month)[1]
day = min(last_day_of_month, order.registered_on.day)
elif self.billing_point == self.FIXED_DATE:
day = 1
else:
@ -171,6 +174,11 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
year = bp.year - relativedelta.relativedelta(years=1)
if bp.month >= month:
year = bp.year + 1
# handle edge cases of last day of the month:
# e.g. on March is 31 but on April 30
last_day_of_month = calendar.monthrange(year,month)[1]
day = min(last_day_of_month, day)
bp = datetime.date(year=year, month=month, day=day)
elif self.billing_period == self.NEVER:
bp = order.registered_on
@ -179,7 +187,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
if self.on_cancel != self.NOTHING and order.cancelled_on and order.cancelled_on < bp:
bp = order.cancelled_on
return bp
# def aligned(self, date):
# if self.granularity == self.DAILY:
# return date
@ -188,7 +196,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
# 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)
anual_prepay_of_monthly_pricing = bool(
@ -211,7 +219,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
raise NotImplementedError
size = round(size, 2)
return decimal.Decimal(str(size))
def get_pricing_slots(self, ini, end):
day = 1
month = settings.SERVICES_SERVICE_ANUAL_BILLING_MONTH
@ -235,7 +243,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
if next >= end:
break
ini = next
def get_pricing_rdelta(self):
period = self.get_pricing_period()
if period == self.MONTHLY:
@ -244,13 +252,13 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
return relativedelta.relativedelta(years=1)
elif period == self.NEVER:
return None
def generate_discount(self, line, dtype, price):
line.discounts.append(AttrDict(**{
'type': dtype,
'total': price,
}))
def generate_line(self, order, price, *dates, metric=1, discounts=None, computed=False):
"""
discounts: extra discounts to apply
@ -263,7 +271,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
else:
raise AttributeError("WTF is '%s'?" % dates)
discounts = discounts or ()
size = self.get_price_size(ini, end)
if not computed:
price = price * size
@ -277,7 +285,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
'metric': metric,
'discounts': [],
})
if subtotal > price:
plan_discount = price-subtotal
self.generate_discount(line, self._PLAN, plan_discount)
@ -290,7 +298,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
if dprice:
self.generate_discount(line, dtype, dprice)
return line
def assign_compensations(self, givers, receivers, **options):
compensations = []
for order in givers:
@ -313,7 +321,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
if hasattr(order, 'new_billed_until'):
order.billed_until = order.new_billed_until
order.save(update_fields=['billed_until'])
def apply_compensations(self, order, only_beyond=False):
dsize = 0
ini = order.billed_until or order.registered_on
@ -339,7 +347,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
new_end = cend
dsize += self.get_price_size(comp.ini, cend)
return dsize, new_end
def get_register_or_renew_events(self, porders, ini, end):
counter = 0
for order in porders:
@ -354,7 +362,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
if registered != order.billed_until and order.billed_until > ini and order.billed_until <= end:
counter += 1
return counter
def bill_concurrent_orders(self, account, porders, rates, ini, end):
# Concurrent
# Get pricing orders
@ -403,7 +411,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
order, price, ini, end, discounts=discounts, computed=True)
lines.append(line)
return lines
def bill_registered_or_renew_events(self, account, porders, rates):
# Before registration
lines = []
@ -431,7 +439,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
line = self.generate_line(order, price, ini, end, discounts=discounts)
lines.append(line)
return lines
def bill_with_orders(self, orders, account, **options):
# For the "boundary conditions" just think that:
# date(2011, 1, 1) is equivalent to datetime(2011, 1, 1, 0, 0, 0)
@ -458,7 +466,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
end = max(end, bp)
orders_.append(order)
orders = orders_
# Compensation
related_orders = account.orders.filter(service=self.service)
if self.payment_style == self.PREPAY and self.on_cancel == self.COMPENSATE:
@ -504,7 +512,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
line = self.generate_line(order, price, ini, end, discounts=discounts)
lines.append(line)
return lines
def bill_with_metric(self, orders, account, **options):
lines = []
bp = None
@ -512,7 +520,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
prepay_discount = 0
bp = self.get_billing_point(order, bp=bp, **options)
recharged_until = datetime.date.min
if (self.billing_period != self.NEVER and
self.get_pricing_period() == self.NEVER and
self.payment_style == self.PREPAY and order.billed_on):
@ -633,7 +641,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
# Last processed metric for futrue recharges
order.new_billed_metric = metric
return lines
def generate_bill_lines(self, orders, account, **options):
if options.get('proforma', False):
options['commit'] = False