diff --git a/app/main/views/send.py b/app/main/views/send.py index 706f20832..ab6412caf 100644 --- a/app/main/views/send.py +++ b/app/main/views/send.py @@ -793,6 +793,8 @@ def check_messages(service_id, template_id, upload_id, row_index=2): remaining_email_this_year = current_service.email_annual_limit - stats_ytd["email"] # Show annual limit validation over the daily one (even if both are true) + data["send_exceeds_annual_limit"] = False + data["send_exceeds_daily_limit"] = False if data["template"].template_type == "email": if remaining_email_this_year < data["count_of_recipients"]: data["recipients_remaining_messages"] = remaining_email_this_year @@ -801,10 +803,11 @@ def check_messages(service_id, template_id, upload_id, row_index=2): if remaining_sms_this_year < data["count_of_recipients"]: data["recipients_remaining_messages"] = remaining_sms_this_year data["send_exceeds_annual_limit"] = True + else: + data["send_exceeds_daily_limit"] = data["recipients"].sms_fragment_count > data["sms_parts_remaining"] - data["send_exceeds_daily_limit"] = data["recipients"].sms_fragment_count > data["sms_parts_remaining"] else: - data["send_exceeds_daily_limit"] = data["recipients"].sms_fragment_count > data["sms_parts_remaining"] + data["send_exceeds_daily_limit"] = data["recipients"].sms_fragment_count > data["sms_parts_remaining"] if ( data["recipients"].too_many_rows diff --git a/app/templates/partials/check/too-many-email-messages.html b/app/templates/partials/check/too-many-email-messages.html index ff7dd0295..ed75a1ab2 100644 --- a/app/templates/partials/check/too-many-email-messages.html +++ b/app/templates/partials/check/too-many-email-messages.html @@ -6,18 +6,17 @@ {% else %} {% if send_exceeds_annual_limit %} -

{{ _('{} can only send {} more email messages until annual limit resets'.format(current_service.name, recipients_remaining_messages)) }}

+

{{ _('{} can only send {} more email messages until annual limit resets'.format(current_service.name, recipients_remaining_messages)) }}

{{ _('To send some of these messages now, edit the spreadsheet to {} recipients maximum. '.format(recipients_remaining_messages)) }} {{ _('To send to recipients you removed, wait until April 1, {} or contact them some other way.'.format(now().year)) }}

- {% endif %} - {% if send_exceeds_daily_limit %} + {% elif send_exceeds_daily_limit or recipients.more_rows_than_can_send %} {{ _("To request a daily limit above {} emails, {}").format(current_service.message_limit, content_link(_("contact us"), url_for('main.contact'), is_external_link=true)) }}

{{ _('You cannot send all these email messages today') }}

-

+

{{ _("You can try sending these messages after {} Eastern Time. Check {}.").format(time_to_reset[current_lang], content_link(_("your current local time"), _('https://nrc.canada.ca/en/web-clock/'), is_external_link=true))}}

diff --git a/app/templates/partials/check/too-many-sms-message-parts.html b/app/templates/partials/check/too-many-sms-message-parts.html index 1000fe1ac..8ceee2076 100644 --- a/app/templates/partials/check/too-many-sms-message-parts.html +++ b/app/templates/partials/check/too-many-sms-message-parts.html @@ -1,6 +1,6 @@ {% from "components/links.html" import content_link %} -

+

{%- if current_service.trial_mode %} {{ _("Your service is in trial mode. To send more messages, request to go live").format(url_for('main.request_to_go_live', service_id=current_service.id)) }} {% else %} diff --git a/app/templates/views/check/column-errors.html b/app/templates/views/check/column-errors.html index 26cb381b0..b60c6457f 100644 --- a/app/templates/views/check/column-errors.html +++ b/app/templates/views/check/column-errors.html @@ -10,10 +10,12 @@ {% set prefix_txt = _('a column called') %} {% set prefix_plural_txt = _('columns called') %} -{% if send_exceeds_daily_limit %} - {% set page_title = _('These messages exceed your daily limit') %} -{% elif send_exceeds_annual_limit %} +{% if send_exceeds_annual_limit %} {% set page_title = _('These messages exceed the annual limit') %} +{% elif send_exceeds_daily_limit and (sms_parts_remaining <= 0) %} + {% set page_title = _('These messages exceed your daily limit') %} +{% elif send_exceeds_daily_limit or recipients.more_rows_than_can_send %} + {% set page_title = _('These messages exceed your daily limit') %} {% else %} {% set page_title = _('Check there’s a column for each variable') %} {% endif %} @@ -168,7 +170,7 @@

{{ _('You cannot send all these text messages today') {% call banner_wrapper(type='dangerous') %} {% include "partials/check/too-many-email-messages.html" %} {% endcall %} - {% elif recipients.more_rows_than_can_send %} + {% elif recipients.more_rows_than_can_send or send_exceeds_annual_limit %} {% call banner_wrapper(type='dangerous') %} {% include "partials/check/too-many-email-messages.html" %} {% endcall %} diff --git a/tests/app/main/views/test_send.py b/tests/app/main/views/test_send.py index 78df4b10c..bb431810e 100644 --- a/tests/app/main/views/test_send.py +++ b/tests/app/main/views/test_send.py @@ -6,6 +6,7 @@ from io import BytesIO from itertools import repeat from os import path +from unittest.mock import patch from uuid import uuid4 from zipfile import BadZipFile @@ -41,6 +42,7 @@ mock_get_service_letter_template, mock_get_service_template, normalize_spaces, + set_config, ) template_types = ["email", "sms"] @@ -2543,6 +2545,7 @@ def test_check_messages_shows_too_many_sms_messages_errors( mock_get_jobs, mock_s3_download, mock_s3_set_metadata, + mock_notification_counts_client, fake_uuid, num_requested, expected_msg, @@ -2560,6 +2563,10 @@ def test_check_messages_shows_too_many_sms_messages_errors( }, ) + mock_notification_counts_client.get_all_notification_counts_for_year.return_value = { + "sms": 0, + "email": 0, + } with client_request.session_transaction() as session: session["file_uploads"] = { fake_uuid: { @@ -2584,6 +2591,30 @@ def test_check_messages_shows_too_many_sms_messages_errors( assert details == expected_msg +@pytest.fixture +def mock_notification_counts_client(): + with patch("app.main.views.send.notification_counts_client") as mock: + yield mock + + +@pytest.fixture +def mock_daily_sms_fragment_count(): + with patch("app.main.views.send.daily_sms_fragment_count") as mock: + yield mock + + +@pytest.fixture +def mock_daily_email_count(): + with patch("app.main.views.send.daily_email_count") as mock: + yield mock + + +@pytest.fixture +def mock_get_service_template_annual_limits(): + with patch("app.service_api_client.get_service_template") as mock: + yield mock + + @pytest.mark.parametrize( "num_requested,expected_msg", [ @@ -2601,6 +2632,7 @@ def test_check_messages_shows_too_many_email_messages_errors( mock_get_template_statistics, mock_get_job_doesnt_exist, mock_get_jobs, + mock_notification_counts_client, fake_uuid, num_requested, expected_msg, @@ -2617,7 +2649,10 @@ def test_check_messages_shows_too_many_email_messages_errors( "email": {"requested": num_requested, "delivered": 0, "failed": 0}, }, ) - + mock_notification_counts_client.get_all_notification_counts_for_year.return_value = { + "sms": 0, + "email": 0, + } with client_request.session_transaction() as session: session["file_uploads"] = { fake_uuid: { @@ -3401,3 +3436,181 @@ class Object(object): multiple_choise_options = [x.text.strip() for x in options] assert multiple_choise_options == expected_filenames + + +class TestAnnualLimitsSend: + @pytest.mark.parametrize( + "num_being_sent, num_sent_today, num_sent_this_year, expect_to_see_annual_limit_msg, expect_to_see_daily_limit_msg", + [ + # annual limit for mock_get_live_service is 10,000email/10,000sms + # daily limit for mock_get_live_service is 1,000email/1,000sms + # 1000 have already been sent today, trying to send 100 more [over both limits] + (100, 1000, 10000, True, False), + # No sent yet today or this year, trying to send 1001 [over both limits] + (10001, 0, 0, True, False), + # 600 have already been sent this year, trying to send 500 more [over annual limit but not daily] + (500, 0, 9600, True, False), + # No sent yet today or this year, trying to send 1001 [over daily limit but not annual] + (1001, 0, 0, False, True), + # No sent yet today or this year, trying to send 100 [over neither limit] + (100, 0, 0, False, False), + ], + ids=[ + "email_over_both_limits", + "email_over_both_limits2", + "email_over_annual_but_not_daily", + "email_over_daily_but_not_annual", + "email_over_neither", + ], + ) + def test_email_send_fails_approrpiately_when_over_limits( + self, + mocker, + client_request, + mock_get_live_service, # set email_annual_limit and sms_annual_limit to 1000 + mock_get_users_by_service, + mock_get_service_email_template_without_placeholders, + mock_get_template_statistics, + mock_get_job_doesnt_exist, + mock_get_jobs, + mock_s3_set_metadata, + mock_notification_counts_client, + mock_daily_sms_fragment_count, + mock_daily_email_count, + fake_uuid, + num_being_sent, + num_sent_today, + num_sent_this_year, + expect_to_see_annual_limit_msg, + expect_to_see_daily_limit_msg, + ): + mocker.patch( + "app.main.views.send.s3download", + return_value=",\n".join(["email address"] + ([mock_get_users_by_service(None)[0]["email_address"]] * num_being_sent)), + ) + + # mock that `num_sent_this_year` have already been sent this year + mock_notification_counts_client.get_all_notification_counts_for_year.return_value = { + "sms": 1000, # not used in test but needs a value + "email": num_sent_this_year, + } + + # mock that we've already sent `emails_sent_today` emails today + mock_daily_email_count.return_value = num_sent_today + mock_daily_sms_fragment_count.return_value = 900 # not used in test but needs a value + + with client_request.session_transaction() as session: + session["file_uploads"] = { + fake_uuid: { + "template_id": fake_uuid, + "notification_count": 1, + "valid": True, + } + } + + page = client_request.get( + "main.check_messages", + service_id=SERVICE_ONE_ID, + template_id=fake_uuid, + upload_id=fake_uuid, + original_file_name="valid.csv", + _test_page_title=False, + ) + + if expect_to_see_annual_limit_msg: + assert page.find(attrs={"data-testid": "exceeds-annual"}) is not None + else: + assert page.find(attrs={"data-testid": "exceeds-annual"}) is None + + if expect_to_see_daily_limit_msg: + assert page.find(attrs={"data-testid": "exceeds-daily"}) is not None + else: + assert page.find(attrs={"data-testid": "exceeds-daily"}) is None + + @pytest.mark.parametrize( + "num_being_sent, num_sent_today, num_sent_this_year, expect_to_see_annual_limit_msg, expect_to_see_daily_limit_msg", + [ + # annual limit for mock_get_live_service is 10,000email/10,000sms + # daily limit for mock_get_live_service is 1,000email/1,000sms + # 1000 have already been sent today, trying to send 100 more [over both limits] + (100, 1000, 10000, True, False), + # No sent yet today or this year, trying to send 1001 [over both limits] + (10001, 0, 0, True, False), + # 600 have already been sent this year, trying to send 500 more [over annual limit but not daily] + (500, 0, 9600, True, False), + # No sent yet today or this year, trying to send 1001 [over daily limit but not annual] + (1001, 0, 0, False, True), + # No sent yet today or this year, trying to send 100 [over neither limit] + (100, 0, 0, False, False), + ], + ids=[ + "sms_over_both_limits", + "sms_over_both_limits2", + "sms_over_annual_but_not_daily", + "sms_over_daily_but_not_annual", + "sms_over_neither", + ], + ) + def test_sms_send_fails_approrpiately_when_over_limits( + self, + mocker, + client_request, + mock_get_live_service, # set email_annual_limit and sms_annual_limit to 1000 + mock_get_users_by_service, + mock_get_service_sms_template_without_placeholders, + mock_get_template_statistics, + mock_get_job_doesnt_exist, + mock_get_jobs, + mock_s3_set_metadata, + mock_notification_counts_client, + mock_daily_sms_fragment_count, + mock_daily_email_count, + fake_uuid, + num_being_sent, + num_sent_today, + num_sent_this_year, + expect_to_see_annual_limit_msg, + expect_to_see_daily_limit_msg, + ): + mocker.patch( + "app.main.views.send.s3download", + return_value=",\n".join(["phone number"] + ([mock_get_users_by_service(None)[0]["mobile_number"]] * num_being_sent)), + ) + + # mock that `num_sent_this_year` have already been sent this year + mock_notification_counts_client.get_all_notification_counts_for_year.return_value = { + "sms": num_sent_this_year, + "email": 1000, # not used in test but needs a value + } + + # mock that we've already sent `emails_sent_today` emails today + mock_daily_email_count.return_value = 900 # not used in test but needs a value + mock_daily_sms_fragment_count.return_value = num_sent_today + + with client_request.session_transaction() as session: + session["file_uploads"] = { + fake_uuid: { + "template_id": fake_uuid, + "notification_count": 1, + "valid": True, + } + } + + page = client_request.get( + "main.check_messages", + service_id=SERVICE_ONE_ID, + template_id=fake_uuid, + upload_id=fake_uuid, + original_file_name="valid.csv", + _test_page_title=False, + ) + + if expect_to_see_annual_limit_msg: + assert page.find(attrs={"data-testid": "exceeds-annual"}) is not None + else: + assert page.find(attrs={"data-testid": "exceeds-annual"}) is None + + if expect_to_see_daily_limit_msg: + assert page.find(attrs={"data-testid": "exceeds-daily"}) is not None + else: + assert page.find(attrs={"data-testid": "exceeds-daily"}) is None diff --git a/tests/conftest.py b/tests/conftest.py index 94ff0ca5b..21ed1f782 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -550,7 +550,14 @@ def fake_uuid(): @pytest.fixture(scope="function") def mock_get_service(mocker, api_user_active): def _get(service_id): - service = service_json(service_id, users=[api_user_active["id"]], message_limit=50, sms_daily_limit=20) + service = service_json( + service_id, + users=[api_user_active["id"]], + message_limit=50, + sms_daily_limit=20, + email_annual_limit=1000, + sms_annual_limit=1000, + ) return {"data": service} return mocker.patch("app.service_api_client.get_service", side_effect=_get) @@ -675,7 +682,9 @@ def mock_service_email_from_is_unique(mocker): @pytest.fixture(scope="function") def mock_get_live_service(mocker, api_user_active): def _get(service_id): - service = service_json(service_id, users=[api_user_active["id"]], restricted=False) + service = service_json( + service_id, users=[api_user_active["id"]], restricted=False, sms_annual_limit=10000, email_annual_limit=10000 + ) return {"data": service} return mocker.patch("app.service_api_client.get_service", side_effect=_get) @@ -971,6 +980,21 @@ def _get(service_id, template_id, version=None): return mocker.patch("app.service_api_client.get_service_template", side_effect=_get) +@pytest.fixture(scope="function") +def mock_get_service_sms_template_without_placeholders(mocker): + def _get(service_id, template_id, version=None): + template = template_json( + service_id, + template_id, + "Two week reminder", + "sms", + "Yo.", + ) + return {"data": template} + + return mocker.patch("app.service_api_client.get_service_template", side_effect=_get) + + @pytest.fixture(scope="function") def mock_get_service_letter_template(mocker, content=None, subject=None, postage="second"): def _get(service_id, template_id, version=None, postage=postage):