Files
notifications-admin/notifications_utils/letter_timings.py
Alex Janousek 8d33f28b76 Refactored reports to use pregenerated docs instead (#2831)
* Refactored reports to use pregenerated docs instead

* Fixed e2e test

* Fixed anothr bug

* Cleanup

* Fixed timezone conversion

* Updated ref files

* Updated reference files, refreshed ui/ux for report generation. Buttons toggle on and off based on if report exists

* Fixed linting errors, removed pytz

* Fixed test failure

* e2e test fix

* Speeding up unit tests

* Removed python time library that was causing performance issues with unit tests

* Updated poetry lock

* Unit test improvements

* Made change that ken reccomended
2025-08-15 15:02:54 -04:00

168 lines
5.0 KiB
Python

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