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