Monthly billing - part 1

This is still a work in progress but it would be good to get some eyes on it.
This commit includes creating and updating a row in the monthly billing table and a method to fetch the results.
There is a command to populate the monthly billing for a service and month so we can try it out.
The total cost at the moment are wrong, they do not take into account the free allowance - see notes below about adding that to the table.
Left to do:
create a nightly task to run to update the monthly totals.
create an endpoint to return the yearly billing, the current day will need to be calculated on the fly and added to the totals.
Add the free allowance into the total costs.
This commit is contained in:
Rebecca Law
2017-07-18 18:21:35 +01:00
parent 4b05c32b62
commit 9400988d72
13 changed files with 240 additions and 37 deletions

View File

@@ -5,7 +5,8 @@ from flask.ext.script import Command, Manager, Option
from app import db
from app.models import (PROVIDERS, Service, User, NotificationHistory)
from app.dao.monthly_billing_dao import create_or_update_monthly_billing_sms, get_monthly_billing_sms
from app.models import (PROVIDERS, User)
from app.dao.services_dao import (
delete_service_and_all_associated_db_objects,
dao_fetch_all_services_by_user
@@ -146,3 +147,18 @@ class CustomDbScript(Command):
print('Committed {} updates at {}'.format(len(result), datetime.utcnow()))
db.session.commit()
result = db.session.execute(subq_hist).fetchall()
class PopulateMonthlyBilling(Command):
option_list = (
Option('-s', '-service-id', dest='service_id',
help="Service id to populate monthly billing for"),
Option('-m', '-month', dest="month", help="Use for integer value for month, e.g. 7 for July"),
Option('-y', '-year', dest="year", help="Use for integer value for year, e.g. 2017")
)
def run(self, service_id, month, year):
create_or_update_monthly_billing_sms(service_id, datetime(int(year), int(month), 1))
results = get_monthly_billing_sms(service_id, datetime(int(year), int(month), 1))
print("Finished populating data for {} for service id {}".format(month, service_id))
print(results.monthly_totals)

View File

@@ -16,3 +16,16 @@ def get_april_fools(year):
"""
return pytz.timezone('Europe/London').localize(datetime(year, 4, 1, 0, 0, 0)).astimezone(pytz.UTC).replace(
tzinfo=None)
def get_month_start_end_date(month_year):
"""
This function return the start and date of the month_year as UTC,
:param month_year: the datetime to calculate the start and end date for that month
:return: start_date, end_date, month
"""
import calendar
_, num_days = calendar.monthrange(month_year.year, month_year.month)
first_day = datetime(month_year.year, month_year.month, 1, 0, 0, 0)
last_day = datetime(month_year.year, month_year.month, num_days, 23, 59, 59, 99999)
return first_day, last_day

View File

@@ -1,6 +1,44 @@
from datetime import datetime
from app import db
from app.dao.notification_usage_dao import get_billing_data_for_month
from app.models import MonthlyBilling, SMS_TYPE
def update_monthly_billing(monthly_billing):
db.session.add(monthly_billing)
def create_or_update_monthly_billing_sms(service_id, billing_month):
monthly = get_billing_data_for_month(service_id=service_id, billing_month=billing_month)
# update monthly
monthly_totals = _monthly_billing_data_to_json(monthly)
row = MonthlyBilling.query.filter_by(year=billing_month.year,
month=datetime.strftime(billing_month, "%B"),
notification_type='sms').first()
if row:
row.monthly_totals = monthly_totals
else:
row = MonthlyBilling(service_id=service_id,
notification_type=SMS_TYPE,
year=billing_month.year,
month=datetime.strftime(billing_month, "%B"),
monthly_totals=monthly_totals)
db.session.add(row)
db.session.commit()
def get_monthly_billing_sms(service_id, billing_month):
monthly = MonthlyBilling.query.filter_by(service_id=service_id,
year=billing_month.year,
month=datetime.strftime(billing_month, "%B"),
notification_type=SMS_TYPE).first()
return monthly
def _monthly_billing_data_to_json(monthly):
# ('April', 6, 1, False, 'sms', 0.014)
# (month, billing_units, rate_multiplier, international, notification_type, rate)
# total cost must take into account the free allowance.
# might be a good idea to capture free allowance in this table
return [{"billing_units": x[1],
"rate_multiplier": x[2],
"international": x[3],
"rate": x[5],
"total_cost": (x[1] * x[2]) * x[5]} for x in monthly]

View File

@@ -6,7 +6,7 @@ from sqlalchemy import func, case, cast
from sqlalchemy import literal_column
from app import db
from app.dao.date_util import get_financial_year
from app.dao.date_util import get_financial_year, get_month_start_end_date
from app.models import (NotificationHistory,
Rate,
NOTIFICATION_STATUS_TYPES_BILLABLE,
@@ -35,6 +35,21 @@ def get_yearly_billing_data(service_id, year):
return sum(result, [])
@statsd(namespace="dao")
def get_billing_data_for_month(service_id, billing_month):
start_date, end_date = get_month_start_end_date(billing_month)
rates = get_rates_for_year(start_date, end_date, SMS_TYPE)
result = []
# so the start end date in the query are the valid from the rate, not the month - this is going to take some thought
for r, n in zip(rates, rates[1:]):
result.extend(sms_billing_data_per_month_query(r.rate, service_id, max(r.valid_from, start_date),
min(n.valid_from, end_date)))
result.extend(
sms_billing_data_per_month_query(rates[-1].rate, service_id, max(rates[-1].valid_from, start_date), end_date))
return result
@statsd(namespace="dao")
def get_monthly_billing_data(service_id, year):
start_date, end_date = get_financial_year(year)

View File

@@ -9,3 +9,8 @@ def create_provider_rates(provider_identifier, valid_from, rate):
provider_rates = ProviderRates(provider_id=provider.id, valid_from=valid_from, rate=rate)
db.session.add(provider_rates)
@transactional
def create_sms_rate(rate):
db.session.add(rate)

View File

@@ -18,7 +18,7 @@ from app.dao.templates_dao import dao_get_template_by_id
from app.models import SMS_TYPE, KEY_TYPE_TEST, BRANDING_ORG, EMAIL_TYPE, NOTIFICATION_TECHNICAL_FAILURE, \
NOTIFICATION_SENT, NOTIFICATION_SENDING
from app.celery.statistics_tasks import record_initial_job_statistics, create_initial_notification_statistic_tasks
from app.celery.statistics_tasks import create_initial_notification_statistic_tasks
def send_sms_to_provider(notification):

View File

@@ -1261,3 +1261,12 @@ class MonthlyBilling(db.Model):
__table_args__ = (
UniqueConstraint('service_id', 'month', 'year', 'notification_type', name='uix_monthly_billing'),
)
def serialized(self):
return {
"month": self.month,
"year": self.year,
"service_id": str(self.service_id),
"notification_type": self.notification_type,
"monthly_totals": self.monthly_totals
}

View File

@@ -68,7 +68,6 @@ from app.schemas import (
user_schema,
permission_schema,
notification_with_template_schema,
notification_with_personalisation_schema,
notifications_filter_schema,
detailed_service_schema
)