diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index 6001660df..d169de9d2 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -3,6 +3,7 @@ from collections import namedtuple from datetime import datetime from functools import partial from itertools import groupby +from operator import itemgetter from flask import ( Response, @@ -418,16 +419,8 @@ def get_free_paid_breakdown_for_billable_units(year, free_sms_fragment_limit, bi [billing_month for billing_month in sms_units if billing_month['month'] == month] ) - LetterDetails = namedtuple('LetterDetails', ['billing_units', 'rate', 'cost', 'postage']) - - letter_billing = [LetterDetails(billing_units=x['billing_units'], - rate=x['rate'], - cost=(x['billing_units'] * x['rate']), - postage=x['postage']) - for x in letter_units if x['month'] == month] - - if letter_billing: - letter_billing.sort(key=lambda x: (x.postage, x.rate)) + letter_units_for_month = [x for x in letter_units if x['month'] == month] + letter_billing = format_letter_details_for_month(letter_units_for_month) letter_total = 0 for x in letter_billing: @@ -443,6 +436,49 @@ def get_free_paid_breakdown_for_billable_units(year, free_sms_fragment_limit, bi } +def format_letter_details_for_month(letter_units_for_month): + # Format postage descriptions in letter units e.g. to 'international' not 'europe' + for month in letter_units_for_month: + for k, v in month.items(): + if k == 'postage': + month[k] = get_postage_description(v) + + # letter_units_for_month must be sorted before international postage values can be aggregated + postage_order = {'first class': 0, 'second class': 1, 'international': 2} + letter_units_for_month.sort(key=lambda x: (postage_order[x['postage']], x['rate'])) + + LetterDetails = namedtuple('LetterDetails', ['billing_units', 'rate', 'cost', 'postage_description']) + + # Aggregate the rows for international letters which have the same rate + result = [] + for _key, rate_group in groupby(letter_units_for_month, key=itemgetter('postage', 'rate')): + rate_group = list(rate_group) + + letter_details = LetterDetails( + billing_units=sum(x['billing_units'] for x in rate_group), + rate=format_letter_rate(rate_group[0]['rate']), + cost=(sum(x['billing_units'] for x in rate_group) * rate_group[0]['rate']), + postage_description=rate_group[0]['postage'] + ) + + result.append(letter_details) + + return result + + +def get_postage_description(postage): + if postage in ('first', 'second'): + return f'{postage} class' + return 'international' + + +def format_letter_rate(number): + if number >= 1: + return f"£{number:,.2f}" + + return f"{number * 100:.0f}p" + + def get_free_paid_breakdown_for_month( free_sms_fragment_limit, cumulative, diff --git a/app/templates/views/usage.html b/app/templates/views/usage.html index 2ffefa85c..a78c10f79 100644 --- a/app/templates/views/usage.html +++ b/app/templates/views/usage.html @@ -109,8 +109,8 @@ {% endif %} {% for letter in month.letters%} {% if letter.billing_units %} -
  • {{ "{:,} {}".format(letter.billing_units, letter.postage)}} class {{ message_count_label(letter.billing_units, 'letter', '') }}at - {{ '{:.0f}p'.format(letter.rate * 100) }}
  • +
  • {{ "{:,} {}".format(letter.billing_units, letter.postage_description) }} {{ message_count_label(letter.billing_units, 'letter', '') }}at + {{ letter.rate }}
  • {% endif %} {% endfor %} {% if not (month.free or month.paid or month.letters) %} diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index e4633273f..c748f9b92 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -967,7 +967,7 @@ def test_usage_page( assert 'April' in table assert 'February' in table assert 'March' in table - assert '£20.59' in table + assert '£28.99' in table assert '140 free text messages' in table assert '£20.30' in table assert '1,230 text messages at 1.65p' in table @@ -1009,12 +1009,13 @@ def test_usage_page_with_letters( assert 'April' in table assert 'February' in table assert 'March' in table - assert '£20.59' in table + assert '£28.99' in table assert '140 free text messages' in table assert '£20.30' in table assert '1,230 text messages at 1.65p' in table assert '10 second class letters at 31p' in normalize_spaces(table) assert '5 first class letters at 33p' in normalize_spaces(table) + assert '10 international letters at 84p' in normalize_spaces(table) @freeze_time("2012-04-30 12:12:12") @@ -1027,6 +1028,9 @@ def test_usage_page_displays_letters_ordered_by_postage( ): billable_units_resp = [ {'month': 'April', 'notification_type': 'letter', 'rate': 0.5, 'billing_units': 1, 'postage': 'second'}, + {'month': 'April', 'notification_type': 'letter', 'rate': 1, 'billing_units': 1, 'postage': 'europe'}, + {'month': 'April', 'notification_type': 'letter', 'rate': 1, 'billing_units': 2, 'postage': 'rest-of-word'}, + {'month': 'April', 'notification_type': 'letter', 'rate': 1.5, 'billing_units': 7, 'postage': 'europe'}, {'month': 'April', 'notification_type': 'letter', 'rate': 0.3, 'billing_units': 3, 'postage': 'second'}, {'month': 'April', 'notification_type': 'letter', 'rate': 0.5, 'billing_units': 1, 'postage': 'first'}, ] @@ -1040,10 +1044,44 @@ def test_usage_page_displays_letters_ordered_by_postage( row_for_april = page.find('table').find('tr', class_='table-row') postage_details = row_for_april.find_all('li', class_='tabular-numbers') - assert len(postage_details) == 3 + assert len(postage_details) == 5 assert normalize_spaces(postage_details[0].text) == '1 first class letter at 50p' assert normalize_spaces(postage_details[1].text) == '3 second class letters at 30p' assert normalize_spaces(postage_details[2].text) == '1 second class letter at 50p' + assert normalize_spaces(postage_details[3].text) == '3 international letters at £1.00' + assert normalize_spaces(postage_details[4].text) == '7 international letters at £1.50' + + +@freeze_time("2012-07-30 12:12:12") +def test_usage_page_displays_letters_split_by_month_and_postage( + mocker, + client_request, + service_one, + mock_get_usage, + mock_get_free_sms_fragment_limit +): + billable_units_resp = [ + {'month': 'April', 'notification_type': 'letter', 'rate': 0.5, 'billing_units': 1, 'postage': 'second'}, + {'month': 'April', 'notification_type': 'letter', 'rate': 1, 'billing_units': 1, 'postage': 'europe'}, + {'month': 'May', 'notification_type': 'letter', 'rate': 1, 'billing_units': 7, 'postage': 'europe'}, + {'month': 'May', 'notification_type': 'letter', 'rate': 0.5, 'billing_units': 3, 'postage': 'second'}, + {'month': 'May', 'notification_type': 'letter', 'rate': 0.7, 'billing_units': 1, 'postage': 'first'}, + ] + mocker.patch('app.billing_api_client.get_billable_units', return_value=billable_units_resp) + service_one['permissions'].append('letter') + page = client_request.get( + 'main.usage', + service_id=SERVICE_ONE_ID, + ) + + april_row = normalize_spaces(page.find('table').find_all('tr')[1].text) + may_row = normalize_spaces(page.find('table').find_all('tr')[2].text) + + assert '1 second class letter at 50p' in april_row + assert '1 international letter at £1.00' in april_row + assert '1 first class letter at 70p' in may_row + assert '3 second class letters at 50p' in may_row + assert '7 international letters at £1.00' in may_row def test_usage_page_with_year_argument( diff --git a/tests/conftest.py b/tests/conftest.py index 66c5c08e6..88a96f8d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2627,7 +2627,21 @@ def mock_get_billable_units(mocker): 'rate': 0.33, 'billing_units': 5, 'postage': 'first', - } + }, + { + 'month': 'February', + 'notification_type': 'letter', + 'rate': 0.84, + 'billing_units': 3, + 'postage': 'europe', + }, + { + 'month': 'February', + 'notification_type': 'letter', + 'rate': 0.84, + 'billing_units': 7, + 'postage': 'rest-of-world', + }, ] return mocker.patch(