from collections import namedtuple from datetime import datetime, time, timedelta from zoneinfo import ZoneInfo from notifications_utils.countries.data import Postage from notifications_utils.timezones import utc_string_to_aware_gmt_datetime LETTER_PROCESSING_DEADLINE = time(17, 30) CANCELLABLE_JOB_LETTER_STATUSES = [ "created", "cancelled", "virus-scan-failed", "validation-failed", "technical-failure", "pending-virus-check", ] def set_gmt_hour(day, hour): return ( day.astimezone(ZoneInfo("Europe/London")) .replace(hour=hour, minute=0) .astimezone(ZoneInfo("UTC")) ) def get_next_work_day(date, non_working_days=None): next_day = date + timedelta(days=1) if non_working_days and non_working_days.is_work_day( date=next_day.date(), ): return next_day return get_next_work_day(next_day) def get_next_dvla_working_day(date): """ Printing takes place monday to friday, excluding bank holidays """ return get_next_work_day(date) def get_next_royal_mail_working_day(date): """ Royal mail deliver letters on monday to saturday """ return get_next_work_day(date) def get_delivery_day(date, *, days_to_deliver): next_day = get_next_royal_mail_working_day(date) if days_to_deliver == 1: return next_day return get_delivery_day(next_day, days_to_deliver=(days_to_deliver - 1)) def get_min_and_max_days_in_transit(postage): return { # first class post is printed earlier in the day, so will # actually transit on the printing day, and be delivered the next # day, so effectively spends no full days in transit "first": (0, 0), "second": (1, 2), Postage.EUROPE: (3, 5), Postage.REST_OF_WORLD: (5, 7), }[postage] def get_earliest_and_latest_delivery(print_day, postage): for days_to_transit in get_min_and_max_days_in_transit(postage): yield get_delivery_day(print_day, days_to_deliver=1 + days_to_transit) def get_letter_timings(upload_time, postage): LetterTimings = namedtuple( "LetterTimings", "printed_by, is_printed, earliest_delivery, latest_delivery" ) # shift anything after 5:30pm to the next day processing_day = utc_string_to_aware_gmt_datetime(upload_time) + timedelta( hours=6, minutes=30 ) print_day = get_next_dvla_working_day(processing_day) earliest_delivery, latest_delivery = get_earliest_and_latest_delivery( print_day, postage ) # print deadline is 3pm BST printed_by = set_gmt_hour(print_day, hour=15) now = ( datetime.utcnow() .replace(tzinfo=ZoneInfo("UTC")) .astimezone(ZoneInfo("Europe/London")) ) return LetterTimings( printed_by=printed_by, is_printed=(now > printed_by), earliest_delivery=set_gmt_hour(earliest_delivery, hour=16), latest_delivery=set_gmt_hour(latest_delivery, hour=16), ) def letter_can_be_cancelled(notification_status, notification_created_at): """ If letter does not have status of created or pending-virus-check => can't be cancelled (it has already been processed) If it's after 5.30pm local time and the notification was created today before 5.30pm local time => can't be cancelled (it will already be zipped up to be sent) """ if notification_status not in ("created", "pending-virus-check"): return False if too_late_to_cancel_letter(notification_created_at): return False return True def too_late_to_cancel_letter(notification_created_at): time_created_at = notification_created_at day_created_on = time_created_at.date() current_time = datetime.utcnow() current_day = current_time.date() if ( _after_letter_processing_deadline() and _notification_created_before_today_deadline(notification_created_at) ): return True if ( _notification_created_before_that_day_deadline(notification_created_at) and day_created_on < current_day ): return True if (current_day - day_created_on).days > 1: return True def _after_letter_processing_deadline(): current_utc_datetime = datetime.utcnow() bst_time = current_utc_datetime.time() return bst_time >= LETTER_PROCESSING_DEADLINE def _notification_created_before_today_deadline(notification_created_at): current_bst_datetime = datetime.utcnow() todays_deadline = current_bst_datetime.replace( hour=LETTER_PROCESSING_DEADLINE.hour, minute=LETTER_PROCESSING_DEADLINE.minute, ) notification_created_at_in_bst = notification_created_at return notification_created_at_in_bst <= todays_deadline def _notification_created_before_that_day_deadline(notification_created_at): notification_created_at_bst_datetime = notification_created_at created_at_day_deadline = notification_created_at_bst_datetime.replace( hour=LETTER_PROCESSING_DEADLINE.hour, minute=LETTER_PROCESSING_DEADLINE.minute, ) return notification_created_at_bst_datetime <= created_at_day_deadline