From 1d67b55b16692d070c08ed3946da5dab851d644a Mon Sep 17 00:00:00 2001 From: Katie Smith Date: Thu, 22 Nov 2018 11:53:32 +0000 Subject: [PATCH] Add endpoint for cancelling letters --- app/letters/utils.py | 13 +++ app/service/rest.py | 32 ++++++- tests/app/letters/test_letter_utils.py | 25 ++++++ tests/app/service/test_rest.py | 111 +++++++++++++++++++++++++ 4 files changed, 179 insertions(+), 2 deletions(-) diff --git a/app/letters/utils.py b/app/letters/utils.py index 0bf489435..c7e558244 100644 --- a/app/letters/utils.py +++ b/app/letters/utils.py @@ -162,3 +162,16 @@ def _move_s3_object(source_bucket, source_filename, target_bucket, target_filena current_app.logger.info("Moved letter PDF: {}/{} to {}/{}".format( source_bucket, source_filename, target_bucket, target_filename)) + + +def letter_print_day(created_at): + bst_print_datetime = convert_utc_to_bst(created_at) + timedelta(hours=6, minutes=30) + bst_print_date = bst_print_datetime.date() + + current_bst_date = convert_utc_to_bst(datetime.utcnow()).date() + + if bst_print_date >= current_bst_date: + return 'today' + else: + print_date = bst_print_datetime.strftime('%d %B').lstrip('0') + return 'on {}'.format(print_date) diff --git a/app/service/rest.py b/app/service/rest.py index ce0b37048..f1c4f947e 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -7,6 +7,8 @@ from flask import ( current_app, Blueprint ) +from notifications_utils.letter_timings import letter_can_be_cancelled +from notifications_utils.timezones import convert_utc_to_bst from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.exc import NoResultFound @@ -80,7 +82,8 @@ from app.errors import ( InvalidRequest, register_errors ) -from app.models import Service, EmailBranding +from app.letters.utils import letter_print_day +from app.models import LETTER_TYPE, NOTIFICATION_CANCELLED, Service, EmailBranding from app.schema_validation import validate from app.service import statistics from app.service.service_data_retention_schema import ( @@ -103,7 +106,7 @@ from app.schemas import ( notifications_filter_schema, detailed_service_schema ) -from app.utils import pagination_links, convert_utc_to_bst +from app.utils import pagination_links service_blueprint = Blueprint('service', __name__) @@ -384,6 +387,31 @@ def get_notification_for_service(service_id, notification_id): ), 200 +@service_blueprint.route('//notifications//cancel', methods=['POST']) +def cancel_notification_for_service(service_id, notification_id): + notification = notifications_dao.get_notification_by_id(notification_id, service_id) + + if not notification: + raise InvalidRequest('Notification not found', status_code=404) + elif notification.notification_type != LETTER_TYPE: + raise InvalidRequest('Notification cannot be cancelled - only letters can be cancelled', status_code=400) + elif not letter_can_be_cancelled(notification.status, notification.created_at): + print_day = letter_print_day(notification.created_at) + + raise InvalidRequest( + "It’s too late to cancel this letter. Printing started {} at 5.30pm".format(print_day), + status_code=400) + + updated_notification = notifications_dao.update_notification_status_by_id( + notification_id, + NOTIFICATION_CANCELLED, + ) + + return jsonify( + notification_with_template_schema.dump(updated_notification).data + ), 200 + + def search_for_notification_by_to_field(service_id, search_term, statuses, notification_type): results = notifications_dao.dao_get_notifications_by_to_field( service_id=service_id, diff --git a/tests/app/letters/test_letter_utils.py b/tests/app/letters/test_letter_utils.py index 32f3f9df9..7b989a582 100644 --- a/tests/app/letters/test_letter_utils.py +++ b/tests/app/letters/test_letter_utils.py @@ -10,6 +10,7 @@ from app.letters.utils import ( get_bucket_name_and_prefix_for_notification, get_letter_pdf_filename, get_letter_pdf, + letter_print_day, upload_letter_pdf, ScanErrorType, move_failed_pdf, get_folder_name ) @@ -274,3 +275,27 @@ def test_get_folder_name_in_british_summer_time(notify_api, freeze_date, expecte def test_get_folder_name_returns_empty_string_for_test_letter(): assert '' == get_folder_name(datetime.utcnow(), is_test_or_scan_letter=True) + + +@freeze_time('2017-07-07 20:00:00') +def test_letter_print_day_returns_today_if_letter_was_printed_after_1730_yesterday(): + created_at = datetime(2017, 7, 6, 17, 30) + assert letter_print_day(created_at) == 'today' + + +@freeze_time('2017-07-07 16:30:00') +def test_letter_print_day_returns_today_if_letter_was_printed_today(): + created_at = datetime(2017, 7, 7, 12, 0) + assert letter_print_day(created_at) == 'today' + + +@pytest.mark.parametrize('created_at, formatted_date', [ + (datetime(2017, 7, 5, 16, 30), 'on 6 July'), + (datetime(2017, 7, 6, 16, 29), 'on 6 July'), + (datetime(2016, 8, 8, 10, 00), 'on 8 August'), + (datetime(2016, 12, 12, 17, 29), 'on 12 December'), + (datetime(2016, 12, 12, 17, 30), 'on 13 December'), +]) +@freeze_time('2017-07-07 16:30:00') +def test_letter_print_day_returns_formatted_date_if_letter_printed_before_1730_yesterday(created_at, formatted_date): + assert letter_print_day(created_at) == formatted_date diff --git a/tests/app/service/test_rest.py b/tests/app/service/test_rest.py index 13b0cb422..a1a40b3a8 100644 --- a/tests/app/service/test_rest.py +++ b/tests/app/service/test_rest.py @@ -2790,3 +2790,114 @@ def test_get_organisation_for_service_id_return_empty_dict_if_service_not_in_org service_id=fake_uuid ) assert response == {} + + +def test_cancel_notification_for_service_raises_invalid_request_when_notification_is_not_found( + admin_request, + sample_service, + fake_uuid, +): + response = admin_request.post( + 'service.cancel_notification_for_service', + service_id=sample_service.id, + notification_id=fake_uuid, + _expected_status=404 + ) + assert response['message'] == 'Notification not found' + assert response['result'] == 'error' + + +def test_cancel_notification_for_service_raises_invalid_request_when_notification_is_not_a_letter( + admin_request, + sample_notification, +): + response = admin_request.post( + 'service.cancel_notification_for_service', + service_id=sample_notification.service_id, + notification_id=sample_notification.id, + _expected_status=400 + ) + assert response['message'] == 'Notification cannot be cancelled - only letters can be cancelled' + assert response['result'] == 'error' + + +@pytest.mark.parametrize('notification_status', [ + 'cancelled', + 'sending', + 'sent', + 'delivered', + 'pending', + 'failed', + 'technical-failure', + 'temporary-failure', + 'permanent-failure', + 'validation-failed', + 'virus-scan-failed', + 'returned-letter', +]) +@freeze_time('2018-07-07 12:00:00') +def test_cancel_notification_for_service_raises_invalid_request_when_letter_is_in_wrong_state_to_be_cancelled( + admin_request, + sample_letter_notification, + notification_status, +): + sample_letter_notification.status = notification_status + + response = admin_request.post( + 'service.cancel_notification_for_service', + service_id=sample_letter_notification.service_id, + notification_id=sample_letter_notification.id, + _expected_status=400 + ) + assert response['message'] == 'It’s too late to cancel this letter. Printing started today at 5.30pm' + assert response['result'] == 'error' + + +@pytest.mark.parametrize('notification_status', ['created', 'pending-virus-check']) +@freeze_time('2018-07-07 16:00:00') +def test_cancel_notification_for_service_updates_letter_if_letter_is_in_cancellable_state( + admin_request, + sample_letter_notification, + notification_status, +): + sample_letter_notification.status = notification_status + sample_letter_notification.created_at = datetime.now() + + response = admin_request.post( + 'service.cancel_notification_for_service', + service_id=sample_letter_notification.service_id, + notification_id=sample_letter_notification.id, + ) + assert response['status'] == 'cancelled' + + +@freeze_time('2017-12-12 17:30:00') +def test_cancel_notification_for_service_raises_error_if_its_too_late_to_cancel( + admin_request, + sample_letter_notification, +): + sample_letter_notification.created_at = datetime(2017, 12, 11, 17, 0) + + response = admin_request.post( + 'service.cancel_notification_for_service', + service_id=sample_letter_notification.service_id, + notification_id=sample_letter_notification.id, + _expected_status=400 + ) + assert response['message'] == 'It’s too late to cancel this letter. Printing started on 11 December at 5.30pm' + assert response['result'] == 'error' + + +@freeze_time('2018-7-7 16:00:00') +def test_cancel_notification_for_service_updates_letter_if_still_time_to_cancel( + admin_request, + sample_letter_notification, +): + sample_letter_notification.created_at = datetime(2018, 7, 7, 10, 0) + + response = admin_request.post( + 'service.cancel_notification_for_service', + service_id=sample_letter_notification.service_id, + notification_id=sample_letter_notification.id, + ) + assert response['status'] == 'cancelled'