mirror of
https://github.com/GSA/notifications-api.git
synced 2025-12-20 15:31:15 -05:00
272 lines
9.3 KiB
Python
272 lines
9.3 KiB
Python
from datetime import datetime, timedelta
|
|
|
|
from flask import current_app
|
|
from sqlalchemy import Float, Integer
|
|
from sqlalchemy import func, case, cast
|
|
from sqlalchemy import literal_column
|
|
|
|
from app import db
|
|
from app.dao.date_util import get_financial_year, get_month_start_end_date
|
|
from app.models import (NotificationHistory,
|
|
Rate,
|
|
NOTIFICATION_STATUS_TYPES_BILLABLE,
|
|
KEY_TYPE_TEST,
|
|
SMS_TYPE,
|
|
EMAIL_TYPE, Service)
|
|
from app.statsd_decorators import statsd
|
|
from app.utils import get_london_month_from_utc_column
|
|
|
|
|
|
@statsd(namespace="dao")
|
|
def get_yearly_billing_data(service_id, year):
|
|
start_date, end_date = get_financial_year(year)
|
|
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
|
|
|
|
if not rates:
|
|
return []
|
|
|
|
def get_valid_from(valid_from):
|
|
return start_date if valid_from < start_date else valid_from
|
|
|
|
result = []
|
|
for r, n in zip(rates, rates[1:]):
|
|
result.append(
|
|
sms_yearly_billing_data_query(
|
|
r.rate,
|
|
service_id,
|
|
get_valid_from(r.valid_from),
|
|
n.valid_from
|
|
)
|
|
)
|
|
result.append(
|
|
sms_yearly_billing_data_query(
|
|
rates[-1].rate,
|
|
service_id,
|
|
get_valid_from(rates[-1].valid_from),
|
|
end_date
|
|
)
|
|
)
|
|
|
|
result.append(email_yearly_billing_data_query(service_id, start_date, end_date))
|
|
|
|
return sum(result, [])
|
|
|
|
|
|
@statsd(namespace="dao")
|
|
def get_billing_data_for_month(service_id, start_date, end_date):
|
|
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
|
|
|
|
if not rates:
|
|
return []
|
|
|
|
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)
|
|
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
|
|
|
|
if not rates:
|
|
return []
|
|
|
|
result = []
|
|
for r, n in zip(rates, rates[1:]):
|
|
result.extend(sms_billing_data_per_month_query(r.rate, service_id, r.valid_from, n.valid_from))
|
|
result.extend(sms_billing_data_per_month_query(rates[-1].rate, service_id, rates[-1].valid_from, end_date))
|
|
|
|
return [(datetime.strftime(x[0], "%B"), x[1], x[2], x[3], x[4], x[5]) for x in result]
|
|
|
|
|
|
def billing_data_filter(notification_type, start_date, end_date, service_id):
|
|
return [
|
|
NotificationHistory.notification_type == notification_type,
|
|
NotificationHistory.created_at.between(start_date, end_date),
|
|
NotificationHistory.service_id == service_id,
|
|
NotificationHistory.status.in_(NOTIFICATION_STATUS_TYPES_BILLABLE),
|
|
NotificationHistory.key_type != KEY_TYPE_TEST
|
|
]
|
|
|
|
|
|
def email_yearly_billing_data_query(service_id, start_date, end_date, rate=0):
|
|
result = db.session.query(
|
|
func.count(NotificationHistory.id),
|
|
func.count(NotificationHistory.id),
|
|
rate_multiplier(),
|
|
NotificationHistory.notification_type,
|
|
NotificationHistory.international,
|
|
cast(rate, Integer())
|
|
).filter(
|
|
*billing_data_filter(EMAIL_TYPE, start_date, end_date, service_id)
|
|
).group_by(
|
|
NotificationHistory.notification_type,
|
|
rate_multiplier(),
|
|
NotificationHistory.international
|
|
).first()
|
|
if not result:
|
|
return [(0, 0, 1, EMAIL_TYPE, False, 0)]
|
|
else:
|
|
return [result]
|
|
|
|
|
|
def sms_yearly_billing_data_query(rate, service_id, start_date, end_date):
|
|
result = db.session.query(
|
|
cast(func.sum(NotificationHistory.billable_units * rate_multiplier()), Integer()),
|
|
func.sum(NotificationHistory.billable_units),
|
|
rate_multiplier(),
|
|
NotificationHistory.notification_type,
|
|
NotificationHistory.international,
|
|
cast(rate, Float())
|
|
).filter(
|
|
*billing_data_filter(SMS_TYPE, start_date, end_date, service_id)
|
|
).group_by(
|
|
NotificationHistory.notification_type,
|
|
NotificationHistory.international,
|
|
rate_multiplier()
|
|
).order_by(
|
|
rate_multiplier()
|
|
).all()
|
|
|
|
if not result:
|
|
return [(0, 0, 1, SMS_TYPE, False, rate)]
|
|
else:
|
|
return result
|
|
|
|
|
|
def get_rates_for_daterange(start_date, end_date, notification_type):
|
|
rates = Rate.query.filter(Rate.notification_type == notification_type).order_by(Rate.valid_from).all()
|
|
results = []
|
|
for current_rate, current_rate_expiry_date in zip(rates, rates[1:]):
|
|
if is_between(current_rate.valid_from, start_date, end_date) or \
|
|
is_between(current_rate_expiry_date.valid_from - timedelta(microseconds=1), start_date, end_date):
|
|
results.append(current_rate)
|
|
|
|
if is_between(rates[-1].valid_from, start_date, end_date):
|
|
results.append(rates[-1])
|
|
|
|
if not results:
|
|
for x in reversed(rates):
|
|
if start_date >= x.valid_from:
|
|
results.append(x)
|
|
break
|
|
|
|
return results
|
|
|
|
|
|
def is_between(date, start_date, end_date):
|
|
return start_date <= date <= end_date
|
|
|
|
|
|
def sms_billing_data_per_month_query(rate, service_id, start_date, end_date):
|
|
month = get_london_month_from_utc_column(NotificationHistory.created_at)
|
|
result = db.session.query(
|
|
month.label('month'),
|
|
func.sum(NotificationHistory.billable_units).label('billing_units'),
|
|
rate_multiplier().label('rate_multiplier'),
|
|
NotificationHistory.international,
|
|
NotificationHistory.notification_type,
|
|
cast(rate, Float()).label('rate')
|
|
).filter(
|
|
*billing_data_filter(SMS_TYPE, start_date, end_date, service_id)
|
|
).group_by(
|
|
NotificationHistory.notification_type,
|
|
month,
|
|
NotificationHistory.rate_multiplier,
|
|
NotificationHistory.international
|
|
).order_by(
|
|
month,
|
|
rate_multiplier()
|
|
).all()
|
|
|
|
return result
|
|
|
|
|
|
def rate_multiplier():
|
|
return cast(case([
|
|
(NotificationHistory.rate_multiplier == None, literal_column("'1'")), # noqa
|
|
(NotificationHistory.rate_multiplier != None, NotificationHistory.rate_multiplier), # noqa
|
|
]), Integer())
|
|
|
|
|
|
@statsd(namespace="dao")
|
|
def get_total_billable_units_for_sent_sms_notifications_in_date_range(start_date, end_date, service_id):
|
|
|
|
free_sms_limit = Service.free_sms_fragment_limit()
|
|
|
|
billable_units = 0
|
|
total_cost = 0.0
|
|
|
|
rate_boundaries = discover_rate_bounds_for_billing_query(start_date, end_date)
|
|
for rate_boundary in rate_boundaries:
|
|
result = db.session.query(
|
|
func.sum(
|
|
NotificationHistory.billable_units * func.coalesce(NotificationHistory.rate_multiplier, 1)
|
|
).label('billable_units')
|
|
).filter(
|
|
NotificationHistory.service_id == service_id,
|
|
NotificationHistory.notification_type == 'sms',
|
|
NotificationHistory.created_at >= rate_boundary['start_date'],
|
|
NotificationHistory.created_at < rate_boundary['end_date'],
|
|
NotificationHistory.status.in_(NOTIFICATION_STATUS_TYPES_BILLABLE)
|
|
)
|
|
billable_units_by_rate_boundry = result.scalar()
|
|
if billable_units_by_rate_boundry:
|
|
int_billable_units_by_rate_boundry = int(billable_units_by_rate_boundry)
|
|
if billable_units >= free_sms_limit:
|
|
total_cost += int_billable_units_by_rate_boundry * rate_boundary['rate']
|
|
elif billable_units + int_billable_units_by_rate_boundry > free_sms_limit:
|
|
remaining_free_allowance = abs(free_sms_limit - billable_units)
|
|
total_cost += ((int_billable_units_by_rate_boundry - remaining_free_allowance) * rate_boundary['rate'])
|
|
else:
|
|
total_cost += 0
|
|
billable_units += int_billable_units_by_rate_boundry
|
|
return billable_units, total_cost
|
|
|
|
|
|
def discover_rate_bounds_for_billing_query(start_date, end_date):
|
|
bounds = []
|
|
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
|
|
|
|
def current_valid_from(index):
|
|
return rates[index].valid_from
|
|
|
|
def next_valid_from(index):
|
|
return rates[index + 1].valid_from
|
|
|
|
def current_rate(index):
|
|
return rates[index].rate
|
|
|
|
def append_rate(rate_start_date, rate_end_date, rate):
|
|
bounds.append({
|
|
'start_date': rate_start_date,
|
|
'end_date': rate_end_date,
|
|
'rate': rate
|
|
})
|
|
|
|
if len(rates) == 1:
|
|
append_rate(start_date, end_date, current_rate(0))
|
|
return bounds
|
|
|
|
for i in range(len(rates)):
|
|
# first boundary
|
|
if i == 0:
|
|
append_rate(start_date, next_valid_from(i), current_rate(i))
|
|
|
|
# last boundary
|
|
elif i == (len(rates) - 1):
|
|
append_rate(current_valid_from(i), end_date, current_rate(i))
|
|
|
|
# other boundaries
|
|
else:
|
|
append_rate(current_valid_from(i), next_valid_from(i), current_rate(i))
|
|
|
|
return bounds
|