diff --git a/app/main/views/add_service.py b/app/main/views/add_service.py index a6779fc38..ecf2b9723 100644 --- a/app/main/views/add_service.py +++ b/app/main/views/add_service.py @@ -20,7 +20,8 @@ from app.notify_client.models import InvitedUser from app import ( invite_api_client, user_api_client, - service_api_client + service_api_client, + billing_api_client ) from app.utils import ( @@ -40,17 +41,21 @@ def _add_invited_user_to_service(invited_user): def _create_service(service_name, organisation_type, email_from, form): + free_sms_fragment_limit = current_app.config['DEFAULT_FREE_SMS_FRAGMENT_LIMITS'].get(organisation_type) try: service_id = service_api_client.create_service( service_name=service_name, organisation_type=organisation_type, message_limit=current_app.config['DEFAULT_SERVICE_LIMIT'], - free_sms_fragment_limit=current_app.config['DEFAULT_FREE_SMS_FRAGMENT_LIMITS'].get(organisation_type), + free_sms_fragment_limit=free_sms_fragment_limit, restricted=True, user_id=session['user_id'], email_from=email_from, ) session['service_id'] = service_id + + billing_api_client.create_or_update_free_sms_fragment_limit_for_year(service_id, free_sms_fragment_limit) + return service_id, None except HTTPError as e: if e.status_code == 400 and e.message['name']: diff --git a/app/main/views/dashboard.py b/app/main/views/dashboard.py index ce63fdc00..9901c3fda 100644 --- a/app/main/views/dashboard.py +++ b/app/main/views/dashboard.py @@ -115,7 +115,9 @@ def usage(service_id): return render_template( 'views/usage.html', months=list(get_free_paid_breakdown_for_billable_units( - year, billing_api_client.get_billable_units(service_id, year) + year, + billing_api_client.get_free_sms_fragment_limit_for_year(service_id, year), + billing_api_client.get_billable_units(service_id, year) )), selected_year=year, years=get_tuples_of_financial_years( @@ -123,7 +125,8 @@ def usage(service_id): start=current_financial_year - 1, end=current_financial_year + 1, ), - **calculate_usage(billing_api_client.get_service_usage(service_id, year)) + **calculate_usage(billing_api_client.get_service_usage(service_id, year), + billing_api_client.get_free_sms_fragment_limit_for_year(service_id, year)) ) @@ -287,9 +290,9 @@ def get_dashboard_totals(statistics): return statistics -def calculate_usage(usage): +def calculate_usage(usage, free_sms_fragment_limit): # TODO: Don't hardcode these - get em from the API - sms_free_allowance = 250000 + sms_free_allowance = free_sms_fragment_limit # VB-Progress sms_rate = 0 if len(usage) == 0 else usage[0].get("rate", 0) sms_sent = get_sum_billing_units(breakdown for breakdown in usage if breakdown['notification_type'] == 'sms') @@ -355,14 +358,14 @@ def get_sum_billing_units(billing_units, month=None): return sum(b['billing_units'] * b.get('rate_multiplier', 1) for b in billing_units) -def get_free_paid_breakdown_for_billable_units(year, billing_units): +def get_free_paid_breakdown_for_billable_units(year, free_sms_fragment_limit, billing_units): cumulative = 0 for month in get_months_for_financial_year(year): previous_cumulative = cumulative monthly_usage = get_sum_billing_units(billing_units, month) cumulative += monthly_usage breakdown = get_free_paid_breakdown_for_month( - cumulative, previous_cumulative, + free_sms_fragment_limit, cumulative, previous_cumulative, [billing_month for billing_month in billing_units if billing_month['month'] == month] ) yield { @@ -373,11 +376,12 @@ def get_free_paid_breakdown_for_billable_units(year, billing_units): def get_free_paid_breakdown_for_month( + free_sms_fragment_limit, cumulative, previous_cumulative, monthly_usage ): - allowance = 250000 + allowance = free_sms_fragment_limit total_monthly_billing_units = get_sum_billing_units(monthly_usage) diff --git a/app/main/views/service_settings.py b/app/main/views/service_settings.py index 4c2cef034..3e370437e 100644 --- a/app/main/views/service_settings.py +++ b/app/main/views/service_settings.py @@ -39,7 +39,7 @@ from app.main.forms import ( FreeSMSAllowance, ServiceEditInboundNumberForm, ) -from app import user_api_client, current_service, organisations_client, inbound_number_client +from app import user_api_client, current_service, organisations_client, inbound_number_client, billing_api_client from notifications_utils.formatters import formatted_list @@ -89,6 +89,9 @@ def service_settings(service_id): default_sms_sender = next( (Field(x['sms_sender'], html='escape') for x in sms_senders if x['is_default']), "None" ) + + free_sms_fragment_limit = billing_api_client.get_free_sms_fragment_limit_for_year(service_id) + return render_template( 'views/service-settings.html', organisation=organisation, @@ -103,7 +106,8 @@ def service_settings(service_id): default_letter_contact_block=default_letter_contact_block, letter_contact_details_count=letter_contact_details_count, default_sms_sender=default_sms_sender, - sms_sender_count=sms_sender_count + sms_sender_count=sms_sender_count, + free_sms_fragment_limit=free_sms_fragment_limit ) @@ -704,13 +708,16 @@ def set_organisation_type(service_id): @user_has_permissions(admin_override=True) def set_free_sms_allowance(service_id): - form = FreeSMSAllowance(free_sms_allowance=current_service['free_sms_fragment_limit']) - + form = FreeSMSAllowance(free_sms_allowance=billing_api_client.get_free_sms_fragment_limit_for_year(service_id)) if form.validate_on_submit(): service_api_client.update_service( service_id, + # TODO: Retire this after new end points are added. free_sms_fragment_limit=form.free_sms_allowance.data, ) + form.set_free_sms_allowance = \ + billing_api_client.create_or_update_free_sms_fragment_limit_for_year(service_id, + form.free_sms_allowance.data) return redirect(url_for('.service_settings', service_id=service_id)) return render_template( diff --git a/app/notify_client/billing_api_client.py b/app/notify_client/billing_api_client.py index 2041f74fe..7e303ca7c 100644 --- a/app/notify_client/billing_api_client.py +++ b/app/notify_client/billing_api_client.py @@ -1,4 +1,6 @@ from app.notify_client import NotifyAdminAPIClient +from flask import current_app +from notifications_python_client.errors import HTTPError class BillingAPIClient(NotifyAdminAPIClient): @@ -23,3 +25,47 @@ class BillingAPIClient(NotifyAdminAPIClient): '/service/{0}/billing/yearly-usage-summary'.format(service_id), params=dict(year=year) ) + + def get_free_sms_fragment_limit_for_year(self, service_id, year=None): + try: + if year is None: + result = self.get( + '/service/{0}/billing/free-sms-fragment-limit/current-year'.format(service_id) + ) + else: + result = self.get( + '/service/{0}/billing/free-sms-fragment-limit'.format(service_id), + params=dict(financial_year_start=year) + ) + return result['free_sms_fragment_limit'] + except HTTPError: + current_app.logger.info( + 'Requested free_sms_fragment_limit entry for service {0} and year {1} does not exist' + .format(service_id, year)) + return -1 + + def get_free_sms_fragment_limit_for_all_years(self, service_id, year=None): + try: + return self.get( + '/service/{0}/billing/free-sms-fragment-limit'.format(service_id), + ) + except HTTPError: + current_app.logger.info( + 'No free_sms_fragment_limit entry exists for service {0} ' + .format(service_id, year)) + return [] + + def create_or_update_free_sms_fragment_limit_for_year(self, service_id, free_sms_fragment_limit, year=None): + if year is None: + data = { + "free_sms_fragment_limit": free_sms_fragment_limit, + } + else: + data = { + "financial_year_start": year, + "free_sms_fragment_limit": free_sms_fragment_limit, + } + return self.post( + url='/service/{0}/billing/free-sms-fragment-limit'.format(service_id), + data=data + ) diff --git a/app/templates/views/service-settings.html b/app/templates/views/service-settings.html index 4e279f043..7157affcd 100644 --- a/app/templates/views/service-settings.html +++ b/app/templates/views/service-settings.html @@ -194,7 +194,7 @@ {% endcall %} {% call row() %} {{ text_field('Free text message allowance')}} - {{ text_field('{:,}'.format(current_service.free_sms_fragment_limit or 0)) }} + {{ text_field('{:,}'.format(free_sms_fragment_limit or 0)) }} {{ edit_field('Change', url_for('.set_free_sms_allowance', service_id=current_service.id)) }} {% endcall %} {% call row() %} diff --git a/tests/app/main/views/test_add_service.py b/tests/app/main/views/test_add_service.py index ee584a27a..0d4ead910 100644 --- a/tests/app/main/views/test_add_service.py +++ b/tests/app/main/views/test_add_service.py @@ -31,6 +31,7 @@ def test_should_add_service_and_redirect_to_tour_when_no_services( mock_create_service_template, mock_get_services_with_no_services, api_user_active, + mock_create_or_update_free_sms_fragment_limit, ): response = logged_in_client.post( url_for('main.add_service'), @@ -83,6 +84,7 @@ def test_should_add_service_and_redirect_to_dashboard_when_existing_service( api_user_active, organisation_type, free_allowance, + mock_create_or_update_free_sms_fragment_limit ): response = logged_in_client.post( url_for('main.add_service'), diff --git a/tests/app/main/views/test_dashboard.py b/tests/app/main/views/test_dashboard.py index 35a68dea4..f78ee5b17 100644 --- a/tests/app/main/views/test_dashboard.py +++ b/tests/app/main/views/test_dashboard.py @@ -502,6 +502,7 @@ def test_usage_page( logged_in_client, mock_get_usage, mock_get_billable_units, + mock_get_free_sms_fragment_limit ): response = logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID)) @@ -509,6 +510,7 @@ def test_usage_page( mock_get_billable_units.assert_called_once_with(SERVICE_ONE_ID, 2011) mock_get_usage.assert_called_once_with(SERVICE_ONE_ID, 2011) + mock_get_free_sms_fragment_limit.assert_called_with(SERVICE_ONE_ID, 2011) page = BeautifulSoup(response.data.decode('utf-8'), 'html.parser') @@ -540,11 +542,14 @@ def test_usage_page( def test_usage_page_with_year_argument( logged_in_client, mock_get_usage, - mock_get_billable_units + mock_get_billable_units, + mock_get_free_sms_fragment_limit, + mock_create_or_update_free_sms_fragment_limit ): assert logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID, year=2000)).status_code == 200 mock_get_billable_units.assert_called_once_with(SERVICE_ONE_ID, 2000) mock_get_usage.assert_called_once_with(SERVICE_ONE_ID, 2000) + mock_get_free_sms_fragment_limit.assert_called_with(SERVICE_ONE_ID, 2000) def test_usage_page_for_invalid_year( @@ -558,11 +563,13 @@ def test_future_usage_page( logged_in_client, mock_get_future_usage, mock_get_future_billable_units, + mock_get_free_sms_fragment_limit ): assert logged_in_client.get(url_for('main.usage', service_id=SERVICE_ONE_ID, year=2014)).status_code == 200 mock_get_future_billable_units.assert_called_once_with(SERVICE_ONE_ID, 2014) mock_get_future_usage.assert_called_once_with(SERVICE_ONE_ID, 2014) + mock_get_free_sms_fragment_limit.assert_called_with(SERVICE_ONE_ID, 2014) def _test_dashboard_menu(mocker, app_, usr, service, permissions): @@ -866,9 +873,10 @@ 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): + sms_allowance = 250000 with now: billing_units = get_free_paid_breakdown_for_billable_units( - 2016, [ + 2016, sms_allowance, [ { 'month': 'April', 'international': False, 'rate_multiplier': 1, 'notification_type': 'sms', 'rate': 1.65, 'billing_units': 100000 diff --git a/tests/app/main/views/test_service_settings.py b/tests/app/main/views/test_service_settings.py index 3a7cc9884..4c553ef9e 100644 --- a/tests/app/main/views/test_service_settings.py +++ b/tests/app/main/views/test_service_settings.py @@ -88,7 +88,8 @@ def test_should_show_overview( single_sms_sender, user, expected_rows, - mock_get_inbound_number_for_service + mock_get_inbound_number_for_service, + mock_get_free_sms_fragment_limit ): service_one['permissions'] = ['sms', 'email'] @@ -1592,7 +1593,9 @@ def test_should_set_sms_allowance( mock_update_service, given_allowance, expected_api_argument, + mock_create_or_update_free_sms_fragment_limit ): + response = logged_in_platform_admin_client.post( url_for( 'main.set_free_sms_allowance', @@ -1609,6 +1612,10 @@ def test_should_set_sms_allowance( SERVICE_ONE_ID, free_sms_fragment_limit=expected_api_argument, ) + mock_create_or_update_free_sms_fragment_limit.assert_called_once_with( + SERVICE_ONE_ID, + expected_api_argument + ) def test_switch_service_enable_letters( diff --git a/tests/app/notify_client/test_billing_client.py b/tests/app/notify_client/test_billing_client.py index 250ad24cd..8e3d403a3 100644 --- a/tests/app/notify_client/test_billing_client.py +++ b/tests/app/notify_client/test_billing_client.py @@ -25,3 +25,54 @@ def test_get_get_service_usage_calls_correct_endpoint(mocker, api_user_active): client.get_service_usage(service_id, 2017) mock_get.assert_called_once_with(expected_url, params={'year': 2017}) + + +def test_get_free_sms_fragment_limit_for_current_year_correct_endpoint(mocker, api_user_active): + service_id = uuid.uuid4() + expected_url = '/service/{}/billing/free-sms-fragment-limit/current-year'.format(service_id) + client = BillingAPIClient() + + mock_get = mocker.patch('app.notify_client.billing_api_client.BillingAPIClient.get') + + client.get_free_sms_fragment_limit_for_year(service_id) + mock_get.assert_called_once_with(expected_url) + + +def test_get_free_sms_fragment_limit_for_year_correct_endpoint(mocker, api_user_active): + service_id = uuid.uuid4() + expected_url = '/service/{}/billing/free-sms-fragment-limit'.format(service_id) + client = BillingAPIClient() + + mock_get = mocker.patch('app.notify_client.billing_api_client.BillingAPIClient.get') + + client.get_free_sms_fragment_limit_for_year(service_id, year=1999) + mock_get.assert_called_once_with(expected_url, params={'financial_year_start': 1999}) + + +def test_post_free_sms_fragment_limit_for_current_year_endpoint(mocker, api_user_active): + service_id = uuid.uuid4() + sms_limit_data = {'free_sms_fragment_limit': 1111} + mock_post = mocker.patch('app.notify_client.billing_api_client.BillingAPIClient.post') + client = BillingAPIClient() + + client.create_or_update_free_sms_fragment_limit_for_year(service_id=service_id, free_sms_fragment_limit=1111) + + mock_post.assert_called_once_with( + url='/service/{}/billing/free-sms-fragment-limit'.format(service_id), + data=sms_limit_data + ) + + +def test_post_free_sms_fragment_limit_for_year_endpoint(mocker, api_user_active): + service_id = uuid.uuid4() + sms_limit_data = {'free_sms_fragment_limit': 1111, 'financial_year_start': 2017} + mock_post = mocker.patch('app.notify_client.billing_api_client.BillingAPIClient.post') + client = BillingAPIClient() + + client.create_or_update_free_sms_fragment_limit_for_year(service_id=service_id, + free_sms_fragment_limit=1111, + year=2017) + mock_post.assert_called_once_with( + url='/service/{}/billing/free-sms-fragment-limit'.format(service_id), + data=sms_limit_data + ) diff --git a/tests/conftest.py b/tests/conftest.py index d3a2a1a7d..181d0c02d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2327,3 +2327,17 @@ def mock_get_aggregate_platform_stats(mocker): } return mocker.patch('app.service_api_client.get_aggregate_platform_stats', return_value=stats) + + +@pytest.fixture(scope='function') +def mock_get_free_sms_fragment_limit(mocker): + sample_limit = 250000 + return mocker.patch('app.billing_api_client.get_free_sms_fragment_limit_for_year', + return_value=sample_limit) + + +@pytest.fixture(scope='function') +def mock_create_or_update_free_sms_fragment_limit(mocker): + sample_limit = 250000 + return mocker.patch('app.billing_api_client.create_or_update_free_sms_fragment_limit_for_year', + return_value=sample_limit)