Fix various billing issues discovered by testing
This commit is contained in:
parent
fe0e6d98bf
commit
623d3ae5c4
1 changed files with 34 additions and 24 deletions
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue