Fix various billing issues discovered by testing

This commit is contained in:
fnux 2020-03-09 11:30:31 +01:00
parent fe0e6d98bf
commit 623d3ae5c4
1 changed files with 34 additions and 24 deletions

View File

@ -9,6 +9,7 @@ from django.core.exceptions import ObjectDoesNotExist
import django.db.models.signals as signals
import uuid
import logging
from functools import reduce
from math import ceil
from datetime import timedelta
@ -18,20 +19,28 @@ import uncloud_pay.stripe
from uncloud_pay.helpers import beginning_of_month, end_of_month
from decimal import Decimal
import decimal
# Define DecimalField properties, used to represent amounts of money.
AMOUNT_MAX_DIGITS=10
AMOUNT_DECIMALS=2
# FIXME: check why we need +1 here.
decimal.getcontext().prec = AMOUNT_DECIMALS + 1
# Used to generate bill due dates.
BILL_PAYMENT_DELAY=timedelta(days=10)
# Initialize logger.
logger = logging.getLogger(__name__)
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
class RecurringPeriod(models.TextChoices):
ONE_TIME = 'ONCE', _('Onetime')
PER_YEAR = 'YEAR', _('Per Year')
PER_MONTH = 'MONTH', _('Per Month')
PER_MINUTE = 'MINUTE', _('Per Minute')
PER_WEEK = 'WEEK', _('Per Week')
PER_DAY = 'DAY', _('Per Day')
PER_HOUR = 'HOUR', _('Per Hour')
PER_SECOND = 'SECOND', _('Per Second')
@ -249,13 +258,16 @@ class Bill(models.Model):
# We ignore anything smaller than a day in here.
next_yearly_bill_start_on = None
if previous_bill == None:
next_yearly_bill_start_on = (order.starting_date + timedelta(days=1)).date()
next_yearly_bill_start_on = order.starting_date
elif previous_bill.ending_date <= ending_date:
next_yearly_bill_start_on = (previous_bill.ending_date + timedelta(days=1)).date()
next_yearly_bill_start_on = (previous_bill.ending_date + timedelta(days=1))
# Store for bill generation. One bucket per day of month with a starting bill.
# bucket is a reference here, no need to reassign.
if next_yearly_bill_start_on:
# We want to group orders by date but keep using datetimes.
next_yearly_bill_start_on = next_yearly_bill_start_on.replace(
minute=0, hour=0, second=0, microsecond=0)
bucket = unpaid_orders['yearly'].get(next_yearly_bill_start_on)
if bucket == None:
unpaid_orders['yearly'][next_yearly_bill_start_on] = [order]
@ -273,7 +285,7 @@ class Bill(models.Model):
next_monthly_bill = Bill.objects.create(owner=user,
creation_date=creation_date,
starting_date=starting_date.datetime(), # FIXME: this is a hack!
starting_date=starting_date, # FIXME: this is a hack!
ending_date=ending_date,
due_date=postpaid_due_date)
@ -282,8 +294,7 @@ class Bill(models.Model):
for order in unpaid_orders['monthly_or_less']:
order.bill.add(next_monthly_bill)
# TODO: use logger.
print("Generated monthly bill {} (amount: {}) for user {}."
logger.info("Generated monthly bill {} (amount: {}) for user {}."
.format(next_monthly_bill.uuid, next_monthly_bill.total, user))
# Add to output.
@ -295,9 +306,10 @@ class Bill(models.Model):
# For every starting date, generate new bill.
for next_yearly_bill_start_on in unpaid_orders['yearly']:
# No postpaid for yearly payments.
prepaid_due_date = min(creation_date.date(), next_yearly_bill_start_on) + BILL_PAYMENT_DELAY
# FIXME: a year is not exactly 365 days...
ending_date = next_yearly_bill_start_on + timedelta(days=365)
prepaid_due_date = min(creation_date, next_yearly_bill_start_on) + BILL_PAYMENT_DELAY
# Bump by one year, remove one day.
ending_date = next_yearly_bill_start_on.replace(
year=next_yearly_bill_start_on.year+1) - timedelta(days=1)
next_yearly_bill = Bill.objects.create(owner=user,
creation_date=creation_date,
@ -310,8 +322,7 @@ class Bill(models.Model):
for order in unpaid_orders['yearly'][next_yearly_bill_start_on]:
order.bill.add(next_yearly_bill)
# TODO: use logger.
print("Generated yearly bill {} (amount: {}) for user {}."
logger.info("Generated yearly bill {} (amount: {}) for user {}."
.format(next_yearly_bill.uuid, next_yearly_bill.total, user))
# Add to output.
@ -361,7 +372,7 @@ class BillRecord():
self.recurring_period = order_record.recurring_period
self.description = order_record.description
if self.order.starting_date > self.bill.starting_date:
if self.order.starting_date >= self.bill.starting_date:
self.one_time_price = order_record.one_time_price
else:
self.one_time_price = 0
@ -370,7 +381,7 @@ class BillRecord():
def recurring_count(self):
# Compute billing delta.
billed_until = self.bill.ending_date
if self.order.ending_date != None and self.order.ending_date < self.order.ending_date:
if self.order.ending_date != None and self.order.ending_date <= self.bill.ending_date:
billed_until = self.order.ending_date
billed_from = self.bill.starting_date
@ -387,10 +398,9 @@ class BillRecord():
# TODO: refactor this thing?
# TODO: weekly
if self.recurring_period == RecurringPeriod.PER_YEAR:
# Should always be one, but let's be careful.
# FIXME: a year is not exactly 365 days...
years = (billed_delta / timedelta(days=365))
return Decimal(years)
# XXX: Should always be one => we do not bill for more than one year.
# TODO: check billed_delta is ~365 days.
return 1
elif self.recurring_period == RecurringPeriod.PER_MONTH:
days = ceil(billed_delta / timedelta(days=1))
@ -404,28 +414,28 @@ class BillRecord():
(_, days_in_month) = monthrange(
self.bill.starting_date.year,
self.bill.starting_date.month)
return Decimal(days / days_in_month)
return days / days_in_month
elif self.recurring_period == RecurringPeriod.PER_WEEK:
weeks = ceil(billed_delta / timedelta(week=1))
return Decimal(weeks)
return weeks
elif self.recurring_period == RecurringPeriod.PER_DAY:
days = ceil(billed_delta / timedelta(days=1))
return Decimal(days)
return days
elif self.recurring_period == RecurringPeriod.PER_HOUR:
hours = ceil(billed_delta / timedelta(hours=1))
return Decimal(hours)
return hours
elif self.recurring_period == RecurringPeriod.PER_SECOND:
seconds = ceil(billed_delta / timedelta(seconds=1))
return Decimal(seconds)
return seconds
elif self.recurring_period == RecurringPeriod.ONE_TIME:
return Decimal(0)
return 0
else:
raise Exception('Unsupported recurring period: {}.'.
format(record.recurring_period))
@property
def amount(self):
return self.recurring_price * self.recurring_count + self.one_time_price
return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price
###
# Orders.
@ -440,7 +450,7 @@ class Order(models.Model):
# TODO: enforce ending_date - starting_date to be larger than recurring_period.
creation_date = models.DateTimeField(auto_now_add=True)
starting_date = models.DateTimeField(auto_now_add=True)
starting_date = models.DateTimeField()
ending_date = models.DateTimeField(blank=True,
null=True)