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(