Expand recurring period billing logic for DD/MM/hh/month
This commit is contained in:
parent
4ad737ed90
commit
4e51670a90
2 changed files with 66 additions and 5 deletions
|
@ -4,6 +4,9 @@ from django.contrib.auth import get_user_model
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from math import ceil
|
||||||
|
from datetime import timedelta
|
||||||
|
from calendar import monthrange
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
@ -38,7 +41,7 @@ class Bill(models.Model):
|
||||||
orders = Order.objects.filter(bill=self)
|
orders = Order.objects.filter(bill=self)
|
||||||
for order in orders:
|
for order in orders:
|
||||||
for order_record in order.records:
|
for order_record in order.records:
|
||||||
bill_record = BillRecord(order_record)
|
bill_record = BillRecord(self, order_record)
|
||||||
bill_records.append(bill_record)
|
bill_records.append(bill_record)
|
||||||
|
|
||||||
return bill_records
|
return bill_records
|
||||||
|
@ -47,18 +50,66 @@ class Bill(models.Model):
|
||||||
def total(self):
|
def total(self):
|
||||||
return reduce(lambda acc, record: acc + record.amount(), self.records, 0)
|
return reduce(lambda acc, record: acc + record.amount(), self.records, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def final(self):
|
||||||
|
# A bill is final when its ending date is passed.
|
||||||
|
return self.ending_date < timezone.now()
|
||||||
|
|
||||||
class BillRecord():
|
class BillRecord():
|
||||||
def __init__(self, order_record):
|
def __init__(self, bill, order_record):
|
||||||
self.order = order_record.order.uuid
|
self.bill = bill
|
||||||
|
self.order = order_record.order
|
||||||
self.setup_fee = order_record.setup_fee
|
self.setup_fee = order_record.setup_fee
|
||||||
self.recurring_price = order_record.recurring_price
|
self.recurring_price = order_record.recurring_price
|
||||||
self.recurring_period = order_record.recurring_period
|
self.recurring_period = order_record.recurring_period
|
||||||
self.description = order_record.description
|
self.description = order_record.description
|
||||||
|
|
||||||
def amount(self):
|
def amount(self):
|
||||||
# TODO: Billing logic here!
|
# Compute billing delta.
|
||||||
|
billed_until = self.bill.ending_date
|
||||||
|
if self.order.ending_date != None and self.order.ending_date < self.order.ending_date:
|
||||||
|
billed_until = self.order.ending_date
|
||||||
|
|
||||||
|
billed_from = self.bill.starting_date
|
||||||
|
if self.order.starting_date > self.bill.starting_date:
|
||||||
|
billed_from = self.order.starting_date
|
||||||
|
|
||||||
|
if billed_from > billed_until:
|
||||||
|
# TODO: think about and check edges cases. This should not be
|
||||||
|
# possible.
|
||||||
|
raise Exception('Impossible billing delta!')
|
||||||
|
|
||||||
|
billed_delta = billed_until - billed_from
|
||||||
|
|
||||||
|
# TODO: refactor this thing?
|
||||||
|
# TODO: weekly
|
||||||
|
# TODO: yearly
|
||||||
if self.recurring_period == RecurringPeriod.PER_MONTH:
|
if self.recurring_period == RecurringPeriod.PER_MONTH:
|
||||||
|
days = ceil(billed_delta / timedelta(days=1))
|
||||||
|
|
||||||
|
# XXX: we assume monthly bills for now.
|
||||||
|
if (self.bill.starting_date.year != self.bill.starting_date.year or
|
||||||
|
self.bill.starting_date.month != self.bill.ending_date.month):
|
||||||
|
raise Exception('Bill {} covers more than one month. Cannot bill PER_MONTH.'.
|
||||||
|
format(self.bill.uuid))
|
||||||
|
|
||||||
|
# XXX: minumal length of monthly order is to be enforced somewhere else.
|
||||||
|
(_, days_in_month) = monthrange(
|
||||||
|
self.bill.starting_date.year,
|
||||||
|
self.bill.starting_date.month)
|
||||||
|
adjusted_recurring_price = self.recurring_price / days_in_month
|
||||||
|
recurring_price = adjusted_recurring_price * days
|
||||||
|
|
||||||
return self.recurring_price # TODO
|
return self.recurring_price # TODO
|
||||||
|
elif self.recurring_period == RecurringPeriod.PER_DAY:
|
||||||
|
days = ceil(billed_delta / timedelta(days=1))
|
||||||
|
return self.recurring_price * days
|
||||||
|
elif self.recurring_period == RecurringPeriod.PER_HOUR:
|
||||||
|
hours = ceil(billed_delta / timedelta(hours=1))
|
||||||
|
return self.recurring_price * hours
|
||||||
|
elif self.recurring_period == RecurringPeriod.PER_SECOND:
|
||||||
|
seconds = ceil(billed_delta / timedelta(seconds=1))
|
||||||
|
return self.recurring_price * seconds
|
||||||
else:
|
else:
|
||||||
raise Exception('Unsupported recurring period: {}.'.
|
raise Exception('Unsupported recurring period: {}.'.
|
||||||
format(record.recurring_period))
|
format(record.recurring_period))
|
||||||
|
@ -75,12 +126,14 @@ class BillRecord():
|
||||||
# agree on deal => That's what we want to keep archived.
|
# agree on deal => That's what we want to keep archived.
|
||||||
#
|
#
|
||||||
# /!\ BIG FAT WARNING /!\ #
|
# /!\ BIG FAT WARNING /!\ #
|
||||||
|
|
||||||
class Order(models.Model):
|
class Order(models.Model):
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
owner = models.ForeignKey(get_user_model(),
|
owner = models.ForeignKey(get_user_model(),
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
editable=False)
|
editable=False)
|
||||||
|
|
||||||
|
# TODO: enforce ending_date - starting_date to be larger than recurring_period.
|
||||||
creation_date = models.DateTimeField(auto_now_add=True)
|
creation_date = models.DateTimeField(auto_now_add=True)
|
||||||
starting_date = models.DateTimeField(auto_now_add=True)
|
starting_date = models.DateTimeField(auto_now_add=True)
|
||||||
ending_date = models.DateTimeField(blank=True,
|
ending_date = models.DateTimeField(blank=True,
|
||||||
|
@ -129,6 +182,14 @@ class OrderRecord(models.Model):
|
||||||
def recurring_period(self):
|
def recurring_period(self):
|
||||||
return self.order.recurring_period
|
return self.order.recurring_period
|
||||||
|
|
||||||
|
@property
|
||||||
|
def starting_date(self):
|
||||||
|
return self.order.starting_date
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ending_date(self):
|
||||||
|
return self.order.ending_date
|
||||||
|
|
||||||
class PaymentMethod(models.Model):
|
class PaymentMethod(models.Model):
|
||||||
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||||
owner = models.ForeignKey(get_user_model(),
|
owner = models.ForeignKey(get_user_model(),
|
||||||
|
|
|
@ -22,7 +22,7 @@ class BillSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bill
|
model = Bill
|
||||||
fields = ['owner', 'total', 'due_date', 'creation_date',
|
fields = ['owner', 'total', 'due_date', 'creation_date',
|
||||||
'starting_date', 'ending_date', 'records']
|
'starting_date', 'ending_date', 'records', 'final']
|
||||||
|
|
||||||
class PaymentSerializer(serializers.ModelSerializer):
|
class PaymentSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
Loading…
Reference in a new issue