Update page to handle international stats

This commit is contained in:
Ken Tsang
2017-04-26 17:09:07 +01:00
parent 79b4b37b0e
commit 53e78c2b60
5 changed files with 168 additions and 382 deletions

View File

@@ -102,19 +102,18 @@ def template_history(service_id):
@user_has_permissions('manage_settings', admin_override=True)
def usage(service_id):
year, current_financial_year = requested_and_current_financial_year(request)
monthly_breakdown = get_free_paid_breakdown_for_billable_units(
year, service_api_client.get_billable_units(service_id, year))
return render_template(
'views/usage.html',
months=list(monthly_breakdown),
months=list(get_free_paid_breakdown_for_billable_units(
year, service_api_client.get_billable_units(service_id, year)
)),
selected_year=year,
years=get_tuples_of_financial_years(
partial(url_for, '.usage', service_id=service_id),
start=current_financial_year - 1,
end=current_financial_year + 1,
),
# **calculate_usage(dict(monthly_breakdown))
**calculate_usage(service_api_client.get_service_usage(service_id, year)['data'], monthly_breakdown)
**calculate_usage(service_api_client.get_service_usage(service_id, year))
)
@@ -190,7 +189,7 @@ def get_dashboard_partials(service_id):
**calculate_usage(service_api_client.get_service_usage(
service_id,
get_current_financial_year(),
)['data'])
))
),
}
@@ -202,15 +201,13 @@ def get_dashboard_totals(statistics):
return statistics
def calculate_usage(usage, monthly_breakdown):
def calculate_usage(usage):
# TODO: Don't hardcode these - get em from the API
sms_free_allowance = 250000
sms_rate = 0.0165
sms_rate = usage[0].get("rate", 0)
sms_sent = get_sum_billing_units(breakdown for breakdown in usage if breakdown['notification_type'] == 'sms')
emails_sent = [breakdown["billing_units"] for breakdown in usage if breakdown['notification_type'] == 'email'][0]
sms_sent = get_sum_units(usage.get('sms_breakdown', []))
emails_sent = usage.get('email_count', 0)
sms_breakdown = get_sms_breakdown_adjusted_free_allowance(
usage.get('sms_breakdown', []), sms_free_allowance)
return {
'emails_sent': emails_sent,
'sms_free_allowance': sms_free_allowance,
@@ -218,8 +215,6 @@ def calculate_usage(usage, monthly_breakdown):
'sms_allowance_remaining': max(0, (sms_free_allowance - sms_sent)),
'sms_chargeable': max(0, sms_sent - sms_free_allowance),
'sms_rate': sms_rate,
'sms_breakdown': sms_breakdown,
'sms_free_units_used': usage.get('free_units_used', 0)
}
@@ -266,67 +261,29 @@ def get_months_for_year(start, end, year):
return [datetime(year, month, 1) for month in range(start, end)]
def get_sum_billable_units(usage):
return sum(rate['units'] * rate['multiplier'] for rate in usage)
def get_sum_billing_units(billing_units, month=None):
if month:
return sum(b['billing_units'] * b.get('rate_multiplier', 1) for b in billing_units if b['month'] == month)
return sum(b['billing_units'] * b.get('rate_multiplier', 1) for b in billing_units)
def get_sum_units(usage):
return sum(rate['units'] for rate in usage)
def get_sum_free_units(usage):
return sum(rate.get('free_units', 0) * rate['multiplier'] for rate in usage)
def get_free_paid_breakdown_for_billable_units(year, billable_units):
def get_free_paid_breakdown_for_billable_units(year, billing_units):
cumulative = 0
for month in get_months_for_financial_year(year):
previous_cumulative = cumulative
monthly_usage = get_sum_billable_units(billable_units.get(month, []))
monthly_usage = get_sum_billing_units(billing_units, month)
cumulative += monthly_usage
breakdown = get_free_paid_breakdown_for_month(
cumulative, previous_cumulative, billable_units.get(month, [])
cumulative, previous_cumulative,
[billing_month for billing_month in billing_units if billing_month['month'] == month]
)
yield {
'name': month,
'paid': breakdown['paid'],
'free': breakdown['free'],
'sms_breakdown': breakdown.get('sms_breakdown', []),
'free_units_used': get_sum_free_units(breakdown.get('sms_breakdown', []))
'free': breakdown['free']
}
def get_sms_breakdown_adjusted_free_allowance(breakdown_list, free_allowance=0):
sms_breakdown = []
for breakdown in breakdown_list:
free_units = 0
if free_allowance > 0:
normalised_units = breakdown["units"] * breakdown["multiplier"]
billable_units = normalised_units - free_allowance
if billable_units <= 0:
free_units = breakdown["units"]
free_allowance = abs(billable_units)
else:
free_units = int(free_allowance / breakdown["multiplier"])
if free_allowance < normalised_units:
free_allowance = 0
else:
free_allowance -= normalised_units
adj_breakdown = {
"international": breakdown["international"],
"free_units": free_units,
"units": breakdown["units"] - free_units,
"multiplier": breakdown["multiplier"]
}
sms_breakdown.append(adj_breakdown)
return sms_breakdown
def get_free_paid_breakdown_for_month(
cumulative,
previous_cumulative,
@@ -334,25 +291,23 @@ def get_free_paid_breakdown_for_month(
):
allowance = 250000
total_monthly_billable_units = get_sum_billable_units(monthly_usage)
total_monthly_billing_units = get_sum_billing_units(monthly_usage)
if cumulative < allowance:
return {
'paid': 0,
'free': total_monthly_billable_units,
'free': total_monthly_billing_units,
}
elif previous_cumulative < allowance:
remaining_allowance = allowance - previous_cumulative
return {
'paid': total_monthly_billable_units - remaining_allowance,
'paid': total_monthly_billing_units - remaining_allowance,
'free': remaining_allowance,
'sms_breakdown': get_sms_breakdown_adjusted_free_allowance(monthly_usage, remaining_allowance)
}
else:
return {
'paid': total_monthly_billable_units,
'paid': total_monthly_billing_units,
'free': 0,
'sms_breakdown': get_sms_breakdown_adjusted_free_allowance(monthly_usage)
}

View File

@@ -217,7 +217,7 @@ class ServiceAPIClient(NotifyAdminAPIClient):
def get_service_usage(self, service_id, year=None):
return self.get(
'/service/{0}/fragment/aggregate_statistics'.format(service_id),
'/service/{0}/yearly-usage'.format(service_id),
params=dict(year=year)
)
@@ -231,7 +231,10 @@ class ServiceAPIClient(NotifyAdminAPIClient):
return self.put(url='/service/{}/whitelist'.format(service_id), data=data)
def get_billable_units(self, service_id, year):
return self.get(url='/service/{}/billable-units?year={}'.format(service_id, year))
return self.get(
'/service/{0}/monthly-usage'.format(service_id),
params=dict(year=year)
)
class ServicesBrowsableItem(BrowsableItem):

View File

@@ -82,22 +82,12 @@
smallest=True
) }}
<ul>
{% if month.rate_groups %}
rate_group {{ month.rate_groups|length }}
{% for rate in month.rate_groups %}
{% if rate.free_units %}
<li class="tabular-numbers">{{ "{:,}".format(rate.free_units) }} {% if rate.international %}international{% else %}UK{% endif %} free text messages</li>
{% endif %}
{% if rate.units %}
<li class="tabular-numbers">{{ "{:,}".format(rate.units) }} {% if rate.international %}international{% else %}UK{% endif %} text messages at
{{- ' {:.2f}p'.format(rate.multiplier * sms_rate * 100) }}</li>
{% endif %}
{% endfor %}
{% else %}
{% if month.paid %}
<li class="tabular-numbers">{{ "{:,}".format(month.paid) }} text messages at
{{- ' {:.2f}p'.format(sms_rate * 100) }}</li>
{% endif %}
{% if month.free %}
<li class="tabular-numbers">{{ "{:,}".format(month.free) }} free text messages</li>
{% endif %}
{% if month.paid %}
<li class="tabular-numbers">{{ "{:,}".format(month.paid) }} text messages at
{{- ' {:.2f}p'.format(sms_rate * 100) }}</li>
{% endif %}
{% if not (month.free or month.paid) %}
<li aria-hidden="true"></li>

View File

@@ -11,7 +11,6 @@ from app.main.views.dashboard import (
get_dashboard_totals,
format_monthly_stats_to_list,
get_free_paid_breakdown_for_billable_units,
get_sms_breakdown_adjusted_free_allowance,
aggregate_status_types,
format_template_stats_to_list,
get_tuples_of_financial_years,
@@ -224,95 +223,22 @@ def test_usage_page(
assert normalize_spaces(nav_links[0].text) == '2010 to 2011 financial year'
assert normalize_spaces(nav.find('li', {'aria-selected': 'true'}).text) == '2011 to 2012 financial year'
assert normalize_spaces(nav_links[1].text) == '2012 to 2013 financial year'
assert '123' in cols[0].text
assert 'Emails' in cols[0].text
assert '456,123' in cols[1].text
assert '252,190' in cols[1].text
assert 'Text messages' in cols[1].text
table = page.find('table').text.strip()
assert '249,860 free text messages' in table
assert '40 free text messages' in table
assert '960 text messages at 1.65p' in table
assert 'April' in table
assert 'February' in table
assert 'March' in table
assert '123 free text messages' in table
assert '£3,403.06' in table
assert '249,877 free text messages' in table
assert '206,246 text messages at 1.65p' in table
@freeze_time("2012-03-31 12:12:12")
def test_international_usage_page(
logged_in_client,
mock_get_international_usage,
mock_get_billable_international_units,
):
response = logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID))
assert response.status_code == 200
mock_get_billable_international_units.assert_called_once_with(SERVICE_ONE_ID, 2011)
mock_get_international_usage.assert_called_once_with(SERVICE_ONE_ID, 2011)
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
cols = page.find_all('div', {'class': 'column-half'})
nav = page.find('ul', {'class': 'pill', 'role': 'tablist'})
nav_links = nav.find_all('a')
assert normalize_spaces(nav_links[0].text) == '2010 to 2011 financial year'
assert normalize_spaces(nav.find('li', {'aria-selected': 'true'}).text) == '2011 to 2012 financial year'
assert normalize_spaces(nav_links[1].text) == '2012 to 2013 financial year'
assert '0' in cols[0].text
assert 'Emails' in cols[0].text
assert '252,390' in cols[1].text
assert 'Text messages' in cols[1].text
table = page.find('table').text.strip()
print(table)
assert '249,900 UK free text messages' in table
assert '100 international free text messages' in table
assert '900 international text messages at 1.65p' in table
assert 'April' in table
assert 'March' in table
assert '£20.30' in table
assert '£19.14' in table
@freeze_time("2012-03-31 12:12:12")
def test_international_usage_page(
logged_in_client,
mock_get_international_usage,
mock_get_billable_international_units,
):
response = logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID))
assert response.status_code == 200
mock_get_billable_international_units.assert_called_once_with(SERVICE_ONE_ID, 2011)
mock_get_international_usage.assert_called_once_with(SERVICE_ONE_ID, 2011)
page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser')
cols = page.find_all('div', {'class': 'column-half'})
nav = page.find('ul', {'class': 'pill', 'role': 'tablist'})
nav_links = nav.find_all('a')
assert '252,180' in cols[1].text
assert 'Text messages' in cols[1].text
table = page.find('table').text.strip()
assert '250000 free allowance used' in table
assert '20 international text at 4.95p' in table
assert 'April' in table
assert 'March' in table
assert '£0.99' in table
assert '£15.84' in table
assert '140 free text messages' in table
assert '£20.30' in table
assert '1,230 text messages at 1.65p' in table
def test_usage_page_with_year_argument(
@@ -627,110 +553,40 @@ def test_aggregate_status_types(dict_in, expected_failed, expected_requested):
)
def test_get_free_paid_breakdown_for_billable_units(now, expected_number_of_months):
with now:
assert list(get_free_paid_breakdown_for_billable_units(
2016, {
'April': 100000,
'May': 100000,
'June': 100000,
'February': 1234
}
)) == [
{'name': 'April', 'free': 100000, 'paid': 0},
{'name': 'May', 'free': 100000, 'paid': 0},
{'name': 'June', 'free': 50000, 'paid': 50000},
{'name': 'July', 'free': 0, 'paid': 0},
{'name': 'August', 'free': 0, 'paid': 0},
{'name': 'September', 'free': 0, 'paid': 0},
{'name': 'October', 'free': 0, 'paid': 0},
{'name': 'November', 'free': 0, 'paid': 0},
{'name': 'December', 'free': 0, 'paid': 0},
{'name': 'January', 'free': 0, 'paid': 0},
{'name': 'February', 'free': 0, 'paid': 1234},
{'name': 'March', 'free': 0, 'paid': 0}
][:expected_number_of_months]
@pytest.mark.parametrize(
'usage, expected_breakdown', [
(
[
{"international": False, "multiplier": 1, "units": 240000},
{"international": True, "multiplier": 2, "units": 20},
],
[
{'units': 0, 'free_units': 240000, 'international': False, 'multiplier': 1},
{'units': 0, 'free_units': 20, 'international': True, 'multiplier': 2}
]
),
(
[
{"international": False, "multiplier": 1, "units": 250000},
{"international": True, "multiplier": 2, "units": 20},
],
[
{'units': 0, 'free_units': 250000, 'international': False, 'multiplier': 1},
{'units': 20, 'free_units': 0, 'international': True, 'multiplier': 2}
]
),
(
[
{"international": False, "multiplier": 1, "units": 300000},
{"international": True, "multiplier": 2, "units": 20},
],
[
{'units': 50000, 'free_units': 250000, 'international': False, 'multiplier': 1},
{'units': 20, 'free_units': 0, 'international': True, 'multiplier': 2}
]
),
(
[
{"international": False, "rate": 1.65, "multiplier": 1, "units": 249700},
{"international": True, "rate": 1.65, "multiplier": 1, "units": 100},
{"international": True, "rate": 1.65, "multiplier": 2, "units": 100},
{"international": True, "rate": 1.65, "multiplier": 3, "units": 20},
],
[
{'international': False, 'free_units': 249700, 'units': 0, 'multiplier': 1},
{'international': True, 'free_units': 100, 'units': 0, 'multiplier': 1},
{'international': True, 'free_units': 100, 'units': 0, 'multiplier': 2},
{'international': True, 'free_units': 0, 'units': 20, 'multiplier': 3}
]
),
(
[
{"international": False, "rate": 1.65, "multiplier": 1, "units": 249600},
{"international": True, "rate": 1.65, "multiplier": 1, "units": 100},
{"international": True, "rate": 1.65, "multiplier": 2, "units": 100},
{"international": True, "rate": 1.65,"multiplier": 3, "units": 20},
],
[
{'units': 0, 'free_units': 249600, 'international': False, 'multiplier': 1},
{'units': 0, 'free_units': 100, 'international': True, 'multiplier': 1},
{'units': 0, 'free_units': 100, 'international': True, 'multiplier': 2},
{'units': 0, 'free_units': 20, 'international': True, 'multiplier': 3}
]
),
(
[
{"international": False, "rate": 1.65, "multiplier": 1, "units": 249800},
{"international": True, "rate": 1.65, "multiplier": 1, "units": 100},
{"international": True, "rate": 1.65, "multiplier": 2, "units": 100},
{"international": True, "rate": 1.65,"multiplier": 3, "units": 20},
],
[
{'units': 0, 'free_units': 249800, 'international': False, 'multiplier': 1},
{'units': 0, 'free_units': 100, 'international': True, 'multiplier': 1},
{'units': 50, 'free_units': 50, 'international': True, 'multiplier': 2},
{'units': 20, 'free_units': 0, 'international': True, 'multiplier': 3}
billing_units = get_free_paid_breakdown_for_billable_units(
2016, [
{
'month': 'April', 'international': False, 'rate_multiplier': 1,
'notification_type': 'sms', 'rate': 1.65, 'billing_units': 100000
},
{
'month': 'May', 'international': False, 'rate_multiplier': 1,
'notification_type': 'sms', 'rate': 1.65, 'billing_units': 100000
},
{
'month': 'June', 'international': False, 'rate_multiplier': 1,
'notification_type': 'sms', 'rate': 1.65, 'billing_units': 100000
},
{
'month': 'February', 'international': False, 'rate_multiplier': 1,
'notification_type': 'sms', 'rate': 1.65, 'billing_units': 2000
},
]
)
]
)
def test_get_sms_breakdown_adjusted_free_allowance(usage, expected_breakdown):
free_allowance = 250000
breakdown = get_sms_breakdown_adjusted_free_allowance(usage, free_allowance)
print(breakdown)
assert breakdown == expected_breakdown
assert list(billing_units) == [
{'free': 100000, 'name': 'April', 'paid': 0},
{'free': 100000, 'name': 'May', 'paid': 0},
{'free': 50000, 'name': 'June', 'paid': 50000},
{'free': 0, 'name': 'July', 'paid': 0},
{'free': 0, 'name': 'August', 'paid': 0},
{'free': 0, 'name': 'September', 'paid': 0},
{'free': 0, 'name': 'October', 'paid': 0},
{'free': 0, 'name': 'November', 'paid': 0},
{'free': 0, 'name': 'December', 'paid': 0},
{'free': 0, 'name': 'January', 'paid': 0},
{'free': 0, 'name': 'February', 'paid': 2000},
{'free': 0, 'name': 'March', 'paid': 0}
][:expected_number_of_months]
def test_format_template_stats_to_list_with_no_stats():

View File

@@ -1315,10 +1315,18 @@ def mock_get_template_statistics_for_template(mocker, service_one):
@pytest.fixture(scope='function')
def mock_get_usage(mocker, service_one, fake_uuid):
def _get_usage(service_id, year=None):
return {'data': {
"sms_count": 456123,
"email_count": 123
}}
return [
{"international": False, "rate": 0.0165, "rate_multiplier": 1,
"notification_type": "sms", "billing_units": 251500},
{"international": True, "rate": 0.0165, "rate_multiplier": 1,
"notification_type": "sms", "billing_units": 300},
{"international": True, "rate": 0.0165, "rate_multiplier": 2,
"notification_type": "sms", "billing_units": 150},
{"international": True, "rate": 0.0165, "rate_multiplier": 3,
"notification_type": "sms", "billing_units": 30},
{"international": False, "rate": 0.0165, "notification_type": "email",
"rate_multiplier": None, "billing_units": 1000}
]
return mocker.patch(
'app.service_api_client.get_service_usage', side_effect=_get_usage)
@@ -1327,115 +1335,89 @@ def mock_get_usage(mocker, service_one, fake_uuid):
@pytest.fixture(scope='function')
def mock_get_billable_units(mocker):
def _get_usage(service_id, year):
return {
"April": 123,
"March": 456123
}
return [
{
'month': 'April',
'international': False,
'rate_multiplier': 1,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 249500
},
{
'month': 'April',
'international': True,
'rate_multiplier': 1,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 100
},
{
'month': 'April',
'international': True,
'rate_multiplier': 2,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 100
},
{
'month': 'April',
'international': True,
'rate_multiplier': 3,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 20
},
{
'month': 'March',
'international': False,
'rate_multiplier': 1,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 1000
},
{
'month': 'March',
'international': True,
'rate_multiplier': 1,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 100
},
{
'month': 'March',
'international': True,
'rate_multiplier': 2,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 50
},
{
'month': 'March',
'international': True,
'rate_multiplier': 3,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 10
},
{
'month': 'February',
'international': False,
'rate_multiplier': 1,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 1000
},
{
'month': 'February',
'international': True,
'rate_multiplier': 1,
'notification_type': 'sms',
'rate': 1.65,
'billing_units': 100
},
return mocker.patch(
'app.service_api_client.get_billable_units', side_effect=_get_usage)
@pytest.fixture(scope='function')
def mock_get_international_usage(mocker, service_one, fake_uuid):
# def _get_usage(service_id, year=None):
# return {'data': {
# "sms_count": 252390,
# "email_count": 0
# }}
def _get_usage(service_id, year=None):
return {'data': {
"sms_breakdown": [
{
"international": False,
"rate": 1.65,
"multiplier": 1,
"units": 250900
},
{
"international": True,
"rate": 1.65,
"multiplier": 1,
"units": 1100
},
{
"international": True,
"rate": 1.65,
"multiplier": 2,
"units": 150
},
{
"international": True,
"rate": 1.65,
"multiplier": 3,
"units": 30
}
],
"email_count": 0
}}
return mocker.patch(
'app.service_api_client.get_service_usage', side_effect=_get_usage)
@pytest.fixture(scope='function')
def mock_get_billable_international_units(mocker):
def _get_usage(service_id, year):
return {
"April": [
{
"international": False,
"rate": 1.65,
"multiplier": 1,
"units": 249700
},
{
"international": True,
"rate": 1.65,
"multiplier": 1,
"units": 100
},
{
"international": True,
"rate": 1.65,
"multiplier": 2,
"units": 100
},
{
"international": True,
"rate": 1.65,
"multiplier": 3,
"units": 20
},
],
"March": [
{
"international": False,
"rate": 1.65,
"multiplier": 1,
"units": 1000
},
{
"international": True,
"rate": 1.65,
"multiplier": 1,
"units": 100
},
{
"international": True,
"rate": 1.65,
"multiplier": 2,
"units": 50
},
{
"international": True,
"rate": 1.65,
"multiplier": 3,
"units": 10
},
]
}
]
return mocker.patch(
'app.service_api_client.get_billable_units', side_effect=_get_usage)