diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index c289f60ed..dd23d32b5 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -1,13 +1,15 @@ import uuid +import pytz from datetime import ( datetime, timedelta, date ) +from itertools import groupby from flask import current_app from werkzeug.datastructures import MultiDict -from sqlalchemy import (desc, func, or_, and_, asc) +from sqlalchemy import (desc, func, or_, and_, asc, cast, Text) from sqlalchemy.orm import joinedload from app import db @@ -209,6 +211,30 @@ def get_notifications_for_job(service_id, job_id, filter_dict=None, page=1, page ) +@statsd(namespace="dao") +def get_notification_billable_unit_count_per_month(service_id, year): + start, end = get_financial_year(year) + + notifications = db.session.query( + NotificationHistory.created_at, + NotificationHistory.billable_units + ).order_by( + NotificationHistory.created_at + ).filter( + NotificationHistory.billable_units != 0, + NotificationHistory.service_id == service_id, + NotificationHistory.created_at >= start, + NotificationHistory.created_at < end + ) + + return [ + (month, sum(count for _, count in row)) + for month, row in groupby( + notifications, lambda row: get_bst_month(row[0]) + ) + ] + + @statsd(namespace="dao") def get_notification_with_personalisation(service_id, notification_id, key_type): filter_dict = {'service_id': service_id, 'id': notification_id} @@ -319,3 +345,20 @@ def dao_timeout_notifications(timeout_period_in_seconds): update({'status': NOTIFICATION_TEMPORARY_FAILURE, 'updated_at': update_at}, synchronize_session=False) db.session.commit() return updated + + +def get_financial_year(year): + return get_april_fools(year), get_april_fools(year + 1) + + +def get_april_fools(year): + return datetime( + year, 4, 1, 0, 0, 0, 0, + pytz.timezone("Europe/London") + ).astimezone(pytz.utc) + + +def get_bst_month(datetime): + return pytz.utc.localize(datetime).astimezone( + pytz.timezone("Europe/London") + ).strftime('%B') diff --git a/app/service/rest.py b/app/service/rest.py index 7aa823f80..2cc7dab90 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -310,3 +310,13 @@ def update_whitelist(service_id): else: dao_add_and_commit_whitelisted_contacts(whitelist_objs) return '', 204 + + +@service_blueprint.route('//billable-units') +def get_billable_unit_count(service_id): + try: + return jsonify(notifications_dao.get_notification_billable_unit_count_per_month( + service_id, int(request.args.get('year')) + )) + except TypeError: + return jsonify(result='error', message='No valid year provided'), 400 diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py index 97885f50a..ea7779ecb 100644 --- a/tests/app/dao/test_notification_dao.py +++ b/tests/app/dao/test_notification_dao.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta, date +import pytz import uuid from functools import partial @@ -31,13 +32,15 @@ from app.dao.notifications_dao import ( delete_notifications_created_more_than_a_week_ago, get_notification_by_id, get_notification_for_job, + get_notification_billable_unit_count_per_month, get_notification_with_personalisation, get_notifications_for_job, get_notifications_for_service, update_notification_status_by_id, update_notification_status_by_reference, dao_delete_notifications_and_history_by_id, - dao_timeout_notifications) + dao_timeout_notifications, + get_financial_year) from notifications_utils.template import get_sms_fragment_count @@ -685,6 +688,48 @@ def test_get_all_notifications_for_job_by_status(notify_db, notify_db_session, s assert len(notifications(filter_dict={'status': NOTIFICATION_STATUS_TYPES[:3]}).items) == 3 +def test_get_notification_billable_unit_count_per_month(notify_db, notify_db_session, sample_service): + + for year, month, day in ( + (2017, 1, 15), # ↓ 2016 financial year + (2016, 8, 1), + (2016, 7, 15), + (2016, 4, 15), + (2016, 4, 15), + (2016, 4, 1), # ↓ 2015 financial year + (2016, 3, 31), + (2016, 1, 15) + ): + sample_notification( + notify_db, notify_db_session, service=sample_service, + created_at=datetime( + year, month, day, 0, 0, 0, 0 + ) - timedelta(hours=1, seconds=1) # one second before midnight + ) + + for financial_year, months in ( + ( + 2017, + [] + ), + ( + 2016, + [('April', 2), ('July', 2), ('January', 1)] + ), + ( + 2015, + [('January', 1), ('March', 2)] + ), + ( + 2014, + [] + ) + ): + assert get_notification_billable_unit_count_per_month( + sample_service.id, financial_year + ) == months + + def test_update_notification(sample_notification, sample_template): assert sample_notification.status == 'created' sample_notification.status = 'failed' @@ -1158,3 +1203,11 @@ def test_should_exclude_test_key_notifications_by_default( all_notifications = get_notifications_for_service(sample_service.id, limit_days=1, key_type=KEY_TYPE_TEST).items assert len(all_notifications) == 1 + + +def test_get_financial_year(): + start, end = get_financial_year(2000) + assert start.tzinfo == pytz.utc + assert start.isoformat() == '2000-04-01T00:01:00+00:00' + assert end.tzinfo == pytz.utc + assert end.isoformat() == '2001-04-01T00:01:00+00:00' diff --git a/tests/app/service/test_rest.py b/tests/app/service/test_rest.py index 179d9383a..69e382c4e 100644 --- a/tests/app/service/test_rest.py +++ b/tests/app/service/test_rest.py @@ -1300,3 +1300,27 @@ def test_get_detailed_services_only_includes_todays_notifications(notify_db, not 'email': {'delivered': 0, 'failed': 0, 'requested': 0}, 'sms': {'delivered': 0, 'failed': 0, 'requested': 2} } + + +@freeze_time('2012-12-12T12:00:01') +def test_get_notification_billable_unit_count(client, notify_db, notify_db_session): + notification = create_sample_notification(notify_db, notify_db_session) + response = client.get( + '/service/{}/billable-units?year=2012'.format(notification.service_id), + headers=[create_authorization_header(service_id=notification.service_id)] + ) + assert response.status_code == 200 + assert json.loads(response.get_data(as_text=True)) == { + 'December': 1 + } + + +def test_get_notification_billable_unit_count_missing_year(client, sample_service): + response = client.get( + '/service/{}/billable-units'.format(sample_service.id), + headers=[create_authorization_header(service_id=sample_service.id)] + ) + assert response.status_code == 400 + assert json.loads(response.get_data(as_text=True)) == { + 'message': 'No valid year provided', 'result': 'error' + }