diff --git a/app/service/rest.py b/app/service/rest.py index 180740063..423d5dc9b 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -9,6 +9,7 @@ from flask import ( ) from sqlalchemy.orm.exc import NoResultFound +from app import redis_store from app.dao import notification_usage_dao from app.dao.dao_utils import dao_rollback from app.dao.api_key_dao import ( @@ -16,6 +17,8 @@ from app.dao.api_key_dao import ( get_model_api_keys, get_unsigned_secret, expire_api_key) +from app.dao.date_util import get_financial_year +from app.dao.notification_usage_dao import get_total_billable_units_for_sent_sms_notifications_in_date_range from app.dao.services_dao import ( dao_fetch_service_by_id, dao_fetch_all_services, @@ -58,6 +61,7 @@ from app.schemas import ( ) from app.utils import pagination_links from flask import Blueprint +from notifications_utils.clients.redis import sms_billable_units_cache_key service_blueprint = Blueprint('service', __name__) @@ -440,6 +444,26 @@ def get_monthly_template_stats(service_id): raise InvalidRequest('Year must be a number', status_code=400) +@service_blueprint.route('//yearly-usage-count') +def get_yearly_usage_count(service_id): + try: + cache_key = sms_billable_units_cache_key(service_id) + cached_value = redis_store.get(cache_key) + if cached_value: + return jsonify({'billable_sms_units': cached_value}) + else: + start_date, end_date = get_financial_year(int(request.args.get('year'))) + billable_units = get_total_billable_units_for_sent_sms_notifications_in_date_range( + start_date, + end_date, + service_id) + redis_store.set(cache_key, billable_units, ex=60) + return jsonify({'billable_sms_units': billable_units}) + + except (ValueError, TypeError): + return jsonify(result='error', message='No valid year provided'), 400 + + @service_blueprint.route('//yearly-usage') def get_yearly_billing_usage(service_id): try: diff --git a/tests/app/service/test_rest.py b/tests/app/service/test_rest.py index c12ca4eec..c7ad9495c 100644 --- a/tests/app/service/test_rest.py +++ b/tests/app/service/test_rest.py @@ -62,9 +62,9 @@ def test_get_service_list_with_only_active_flag(client, service_factory): def test_get_service_list_with_user_id_and_only_active_flag( - client, - sample_user, - service_factory + client, + sample_user, + service_factory ): other_user = create_user(email='foo@bar.gov.uk') @@ -663,7 +663,6 @@ def test_add_existing_user_to_another_service_with_all_permissions(notify_api, sample_user): with notify_api.test_request_context(): with notify_api.test_client() as client: - # check which users part of service user_already_in_service = sample_service.users[0] auth_header = create_authorization_header() @@ -738,7 +737,6 @@ def test_add_existing_user_to_another_service_with_send_permissions(notify_api, sample_user): with notify_api.test_request_context(): with notify_api.test_client() as client: - # they must exist in db first user_to_add = User( name='Invited User', @@ -782,7 +780,6 @@ def test_add_existing_user_to_another_service_with_manage_permissions(notify_api sample_user): with notify_api.test_request_context(): with notify_api.test_client() as client: - # they must exist in db first user_to_add = User( name='Invited User', @@ -826,7 +823,6 @@ def test_add_existing_user_to_another_service_with_manage_api_keys(notify_api, sample_user): with notify_api.test_request_context(): with notify_api.test_client() as client: - # they must exist in db first user_to_add = User( name='Invited User', @@ -867,7 +863,6 @@ def test_add_existing_user_to_non_existing_service_returns404(notify_api, sample_user): with notify_api.test_request_context(): with notify_api.test_client() as client: - user_to_add = User( name='Invited User', email_address='invited@digital.cabinet-office.gov.uk', @@ -898,7 +893,6 @@ def test_add_existing_user_to_non_existing_service_returns404(notify_api, def test_add_existing_user_of_service_to_service_returns400(notify_api, notify_db, notify_db_session, sample_service): with notify_api.test_request_context(): with notify_api.test_client() as client: - existing_user_id = sample_service.users[0].id data = {'permissions': ['send_messages', 'manage_service', 'manage_api_keys']} @@ -921,7 +915,6 @@ def test_add_existing_user_of_service_to_service_returns400(notify_api, notify_d def test_add_unknown_user_to_service_returns404(notify_api, notify_db, notify_db_session, sample_service): with notify_api.test_request_context(): with notify_api.test_client() as client: - incorrect_id = 9876 data = {'permissions': ['send_messages', 'manage_service', 'manage_api_keys']} @@ -942,7 +935,7 @@ def test_add_unknown_user_to_service_returns404(notify_api, notify_db, notify_db def test_remove_user_from_service( - notify_db, notify_db_session, client, sample_user_service_permission + notify_db, notify_db_session, client, sample_user_service_permission ): second_user = create_user(email="new@digital.cabinet-office.gov.uk") # Simulates successfully adding a user to the service @@ -962,7 +955,7 @@ def test_remove_user_from_service( def test_remove_non_existant_user_from_service( - client, sample_user_service_permission + client, sample_user_service_permission ): second_user = create_user(email="new@digital.cabinet-office.gov.uk") endpoint = url_for( @@ -998,13 +991,11 @@ def test_cannot_remove_only_user_from_service(notify_api, # This test is just here verify get_service_and_api_key_history that is a temp solution # until proper ui is sorted out on admin app def test_get_service_and_api_key_history(notify_api, notify_db, notify_db_session, sample_service): - from tests.app.conftest import sample_api_key as create_sample_api_key api_key = create_sample_api_key(notify_db, notify_db_session, service=sample_service) with notify_api.test_request_context(): with notify_api.test_client() as client: - auth_header = create_authorization_header() response = client.get( path='/service/{}/history'.format(sample_service.id), @@ -1078,12 +1069,12 @@ def test_get_all_notifications_for_service_in_order(notify_api, notify_db, notif ] ) def test_get_all_notifications_for_service_including_ones_made_by_jobs( - client, - notify_db, - notify_db_session, - sample_service, - include_from_test_key, - expected_count_of_notifications + client, + notify_db, + notify_db_session, + sample_service, + include_from_test_key, + expected_count_of_notifications ): with_job = sample_notification_with_job(notify_db, notify_db_session, service=sample_service) without_job = create_sample_notification(notify_db, notify_db_session, service=sample_service) @@ -1108,10 +1099,10 @@ def test_get_all_notifications_for_service_including_ones_made_by_jobs( def test_get_only_api_created_notifications_for_service( - client, - notify_db, - notify_db_session, - sample_service + client, + notify_db, + notify_db_session, + sample_service ): with_job = sample_notification_with_job(notify_db, notify_db_session, service=sample_service) without_job = create_sample_notification(notify_db, notify_db_session, service=sample_service) @@ -1211,19 +1202,19 @@ def test_get_detailed_service(notify_db, notify_db_session, notify_api, sample_s @pytest.mark.parametrize( 'url, expected_status, expected_json', [ ( - '/service/{}/notifications/monthly?year=2001', - 200, - {'data': {'foo': 'bar'}}, + '/service/{}/notifications/monthly?year=2001', + 200, + {'data': {'foo': 'bar'}}, ), ( - '/service/{}/notifications/monthly?year=baz', - 400, - {'message': 'Year must be a number', 'result': 'error'}, + '/service/{}/notifications/monthly?year=baz', + 400, + {'message': 'Year must be a number', 'result': 'error'}, ), ( - '/service/{}/notifications/monthly', - 400, - {'message': 'Year must be a number', 'result': 'error'}, + '/service/{}/notifications/monthly', + 400, + {'message': 'Year must be a number', 'result': 'error'}, ), ] ) @@ -1452,11 +1443,11 @@ def test_get_notification_billable_unit_count_missing_year(client, sample_servic ('?year=abcd', 400, {'message': 'Year must be a number', 'result': 'error'}), ]) def test_get_service_provider_aggregate_statistics( - client, - sample_service, - query_string, - expected_status, - expected_json, + client, + sample_service, + query_string, + expected_status, + expected_json, ): response = client.get( '/service/{}/fragment/aggregate_statistics{}'.format(sample_service.id, query_string), @@ -1497,11 +1488,11 @@ def test_get_template_stats_by_month_returns_correct_data(notify_db, notify_db_s ('?year=abcd', 400, {'message': 'Year must be a number', 'result': 'error'}), ]) def test_get_template_stats_by_month_returns_error_for_incorrect_year( - client, - sample_service, - query_string, - expected_status, - expected_json + client, + sample_service, + query_string, + expected_status, + expected_json ): response = client.get( '/service/{}/notifications/templates/monthly{}'.format(sample_service.id, query_string), @@ -1729,3 +1720,79 @@ def test_update_service_does_not_call_send_notification_when_restricted_not_chan assert resp.status_code == 200 assert not send_notification_mock.called + + +def test_get_yearly_billing_usage_count_returns_400_if_missing_year(client, sample_service): + response = client.get( + '/service/{}/yearly-usage-count'.format(sample_service.id), + headers=[create_authorization_header()] + ) + assert response.status_code == 400 + assert json.loads(response.get_data(as_text=True)) == { + 'message': 'No valid year provided', 'result': 'error' + } + + +def test_get_yearly_billing_usage_count_returns_400_if_invalid_year(client, sample_service, mocker): + redis_get_mock = mocker.patch('app.service.rest.redis_store.get', return_value=None) + redis_set_mock = mocker.patch('app.service.rest.redis_store.set') + + response = client.get( + '/service/{}/yearly-usage-count?year=HAHAHAHAH'.format(sample_service.id), + headers=[create_authorization_header()] + ) + assert response.status_code == 400 + assert json.loads(response.get_data(as_text=True)) == { + 'message': 'No valid year provided', 'result': 'error' + } + redis_get_mock.assert_called_once_with("{}-sms_billable_units".format(str(sample_service.id))) + redis_set_mock.assert_not_called() + + +def test_get_yearly_billing_usage_count_returns_200_if_year_provided(client, sample_service, mocker): + redis_get_mock = mocker.patch('app.service.rest.redis_store.get', return_value=None) + redis_set_mock = mocker.patch('app.service.rest.redis_store.set') + + start = datetime.utcnow() + end = datetime.utcnow() + timedelta(minutes=10) + mock_query = mocker.patch( + 'app.service.rest.get_total_billable_units_for_sent_sms_notifications_in_date_range', return_value=100 + ) + mock_year = mocker.patch('app.service.rest.get_financial_year', return_value=(start, end)) + response = client.get( + '/service/{}/yearly-usage-count?year=2016'.format(sample_service.id), + headers=[create_authorization_header()] + ) + assert response.status_code == 200 + assert json.loads(response.get_data(as_text=True)) == { + 'billable_sms_units': 100 + } + mock_query.assert_called_once_with(start, end, sample_service.id) + mock_year.assert_called_once_with(2016) + redis_get_mock.assert_called_once_with("{}-sms_billable_units".format(str(sample_service.id))) + redis_set_mock.assert_called_once_with("{}-sms_billable_units".format(str(sample_service.id)), 100, ex=60) + + +def test_get_yearly_billing_usage_count_returns_from_cache_if_present(client, sample_service, mocker): + redis_get_mock = mocker.patch('app.service.rest.redis_store.get', return_value=50) + redis_set_mock = mocker.patch('app.service.rest.redis_store.set') + mock_query = mocker.patch( + 'app.service.rest.get_total_billable_units_for_sent_sms_notifications_in_date_range', return_value=50 + ) + + start = datetime.utcnow() + end = datetime.utcnow() + timedelta(minutes=10) + mock_year = mocker.patch('app.service.rest.get_financial_year', return_value=(start, end)) + + response = client.get( + '/service/{}/yearly-usage-count?year=2016'.format(sample_service.id), + headers=[create_authorization_header()] + ) + assert response.status_code == 200 + assert json.loads(response.get_data(as_text=True)) == { + 'billable_sms_units': 50 + } + redis_get_mock.assert_called_once_with("{}-sms_billable_units".format(str(sample_service.id))) + mock_year.assert_not_called() + mock_query.assert_not_called() + redis_set_mock.assert_not_called()