2020-02-17 15:59:53 +00:00
|
|
|
from datetime import datetime, timedelta
|
2019-03-21 15:40:24 +00:00
|
|
|
from hashlib import sha512
|
|
|
|
|
from base64 import urlsafe_b64encode
|
2017-12-19 14:18:05 +00:00
|
|
|
|
2018-03-19 13:52:01 +00:00
|
|
|
from botocore.exceptions import ClientError as BotoClientError
|
2017-12-19 14:18:05 +00:00
|
|
|
from flask import current_app
|
2020-07-29 14:52:18 +01:00
|
|
|
from notifications_utils.postal_address import PostalAddress
|
2020-04-30 17:49:12 +01:00
|
|
|
|
2018-03-19 13:52:01 +00:00
|
|
|
from notifications_utils.statsd_decorators import statsd
|
2020-02-17 15:59:53 +00:00
|
|
|
from notifications_utils.letter_timings import LETTER_PROCESSING_DEADLINE
|
|
|
|
|
from notifications_utils.timezones import convert_utc_to_bst
|
2017-12-11 11:00:27 +00:00
|
|
|
|
2020-01-24 09:08:27 +00:00
|
|
|
from app import encryption, notify_celery
|
2017-12-11 11:00:27 +00:00
|
|
|
from app.aws import s3
|
2017-12-19 14:18:05 +00:00
|
|
|
from app.config import QueueNames, TaskNames
|
2017-12-13 15:52:38 +00:00
|
|
|
from app.dao.notifications_dao import (
|
2021-02-17 17:47:00 +00:00
|
|
|
dao_get_letters_and_sheets_volume_by_postage,
|
2021-02-22 17:24:37 +00:00
|
|
|
dao_get_letters_to_be_printed,
|
2018-03-23 12:04:37 +00:00
|
|
|
dao_get_notification_by_reference,
|
2021-02-22 17:24:37 +00:00
|
|
|
dao_update_notification,
|
2018-03-19 13:52:01 +00:00
|
|
|
dao_update_notifications_by_reference,
|
2021-02-22 17:24:37 +00:00
|
|
|
get_notification_by_id,
|
|
|
|
|
update_notification_status_by_id,
|
2017-12-13 15:52:38 +00:00
|
|
|
)
|
2021-02-17 17:47:00 +00:00
|
|
|
from app.dao.templates_dao import dao_get_template_by_id
|
2020-02-17 15:59:53 +00:00
|
|
|
from app.letters.utils import get_letter_pdf_filename
|
2018-04-03 12:31:52 +01:00
|
|
|
from app.errors import VirusScanError
|
2019-12-04 16:02:46 +00:00
|
|
|
from app.exceptions import NotificationTechnicalFailureException
|
2018-03-19 13:52:01 +00:00
|
|
|
from app.letters.utils import (
|
2019-10-16 13:35:30 +01:00
|
|
|
get_billable_units_for_letter_page_count,
|
2018-03-19 13:52:01 +00:00
|
|
|
get_reference_from_filename,
|
2018-08-21 18:02:17 +01:00
|
|
|
ScanErrorType,
|
2018-10-16 17:20:34 +01:00
|
|
|
move_failed_pdf,
|
2019-12-04 16:02:46 +00:00
|
|
|
move_sanitised_letter_to_test_or_live_pdf_bucket,
|
2018-10-16 17:20:34 +01:00
|
|
|
move_scan_to_invalid_pdf_bucket,
|
2018-08-21 18:02:17 +01:00
|
|
|
move_error_pdf_to_scan_bucket,
|
2019-09-05 16:45:48 +01:00
|
|
|
get_file_names_from_error_bucket,
|
2018-06-27 16:40:30 +01:00
|
|
|
)
|
2018-03-23 12:04:37 +00:00
|
|
|
from app.models import (
|
2020-05-01 14:26:20 +01:00
|
|
|
INTERNATIONAL_LETTERS,
|
2021-02-17 17:47:00 +00:00
|
|
|
INTERNATIONAL_POSTAGE_TYPES,
|
|
|
|
|
KEY_TYPE_NORMAL,
|
2018-03-23 12:04:37 +00:00
|
|
|
KEY_TYPE_TEST,
|
|
|
|
|
NOTIFICATION_CREATED,
|
|
|
|
|
NOTIFICATION_DELIVERED,
|
2019-12-04 16:02:46 +00:00
|
|
|
NOTIFICATION_PENDING_VIRUS_CHECK,
|
2018-08-21 18:02:17 +01:00
|
|
|
NOTIFICATION_TECHNICAL_FAILURE,
|
2018-10-18 16:01:59 +01:00
|
|
|
NOTIFICATION_VALIDATION_FAILED,
|
|
|
|
|
NOTIFICATION_VIRUS_SCAN_FAILED,
|
2020-06-30 17:54:47 +01:00
|
|
|
POSTAGE_TYPES,
|
2020-07-29 14:52:18 +01:00
|
|
|
RESOLVE_POSTAGE_FOR_FILE_NAME,
|
2021-02-17 17:47:00 +00:00
|
|
|
Service,
|
|
|
|
|
)
|
|
|
|
|
|
2019-04-05 10:26:18 +01:00
|
|
|
from app.cronitor import cronitor
|
2017-12-11 11:00:27 +00:00
|
|
|
|
|
|
|
|
|
2020-05-06 18:23:56 +01:00
|
|
|
@notify_celery.task(bind=True, name="get-pdf-for-templated-letter", max_retries=15, default_retry_delay=300)
|
|
|
|
|
@statsd(namespace="tasks")
|
|
|
|
|
def get_pdf_for_templated_letter(self, notification_id):
|
2017-12-11 11:00:27 +00:00
|
|
|
try:
|
2017-12-12 11:56:42 +00:00
|
|
|
notification = get_notification_by_id(notification_id, _raise=True)
|
2020-04-30 17:49:12 +01:00
|
|
|
letter_filename = get_letter_pdf_filename(
|
|
|
|
|
reference=notification.reference,
|
|
|
|
|
crown=notification.service.crown,
|
2020-09-21 14:40:22 +01:00
|
|
|
created_at=notification.created_at,
|
2020-09-21 13:46:31 +01:00
|
|
|
ignore_folder=notification.key_type == KEY_TYPE_TEST,
|
2020-04-30 17:49:12 +01:00
|
|
|
postage=notification.postage
|
|
|
|
|
)
|
|
|
|
|
letter_data = {
|
|
|
|
|
'letter_contact_block': notification.reply_to_text,
|
|
|
|
|
'template': {
|
|
|
|
|
"subject": notification.template.subject,
|
|
|
|
|
"content": notification.template.content,
|
|
|
|
|
"template_type": notification.template.template_type
|
|
|
|
|
},
|
|
|
|
|
'values': notification.personalisation,
|
|
|
|
|
'logo_filename': notification.service.letter_branding and notification.service.letter_branding.filename,
|
|
|
|
|
'letter_filename': letter_filename,
|
|
|
|
|
"notification_id": str(notification_id),
|
|
|
|
|
'key_type': notification.key_type
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
encrypted_data = encryption.encrypt(letter_data)
|
|
|
|
|
|
|
|
|
|
notify_celery.send_task(
|
2020-05-05 11:17:22 +01:00
|
|
|
name=TaskNames.CREATE_PDF_FOR_TEMPLATED_LETTER,
|
2020-04-30 17:49:12 +01:00
|
|
|
args=(encrypted_data,),
|
|
|
|
|
queue=QueueNames.SANITISE_LETTERS
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
2017-12-11 11:00:27 +00:00
|
|
|
try:
|
|
|
|
|
current_app.logger.exception(
|
2020-05-06 17:43:34 +01:00
|
|
|
f"RETRY: calling create-letter-pdf task for notification {notification_id} failed"
|
2017-12-11 11:00:27 +00:00
|
|
|
)
|
|
|
|
|
self.retry(queue=QueueNames.RETRY)
|
2020-04-30 17:49:12 +01:00
|
|
|
except self.MaxRetriesExceededError:
|
2020-05-06 17:43:34 +01:00
|
|
|
message = f"RETRY FAILED: Max retries reached. " \
|
|
|
|
|
f"The task create-letter-pdf failed for notification id {notification_id}. " \
|
|
|
|
|
f"Notification has been updated to technical-failure"
|
2020-04-30 17:49:12 +01:00
|
|
|
update_notification_status_by_id(notification_id, NOTIFICATION_TECHNICAL_FAILURE)
|
|
|
|
|
raise NotificationTechnicalFailureException(message)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@notify_celery.task(bind=True, name="update-billable-units-for-letter", max_retries=15, default_retry_delay=300)
|
|
|
|
|
@statsd(namespace="tasks")
|
|
|
|
|
def update_billable_units_for_letter(self, notification_id, page_count):
|
|
|
|
|
notification = get_notification_by_id(notification_id, _raise=True)
|
|
|
|
|
|
|
|
|
|
billable_units = get_billable_units_for_letter_page_count(page_count)
|
|
|
|
|
|
|
|
|
|
if notification.key_type != KEY_TYPE_TEST:
|
|
|
|
|
notification.billable_units = billable_units
|
|
|
|
|
dao_update_notification(notification)
|
|
|
|
|
|
2020-05-05 11:17:22 +01:00
|
|
|
current_app.logger.info(
|
|
|
|
|
f"Letter notification id: {notification_id} reference {notification.reference}: "
|
|
|
|
|
f"billable units set to {billable_units}"
|
|
|
|
|
)
|
2017-12-19 14:18:05 +00:00
|
|
|
|
|
|
|
|
|
2020-02-19 14:31:57 +00:00
|
|
|
@notify_celery.task(name='collate-letter-pdfs-to-be-sent')
|
|
|
|
|
@cronitor("collate-letter-pdfs-to-be-sent")
|
|
|
|
|
def collate_letter_pdfs_to_be_sent():
|
2020-02-17 15:59:53 +00:00
|
|
|
"""
|
|
|
|
|
Finds all letters which are still waiting to be sent to DVLA for printing
|
|
|
|
|
|
|
|
|
|
This would usually be run at 5.50pm and collect up letters created between before 5:30pm today
|
|
|
|
|
that have not yet been sent.
|
|
|
|
|
If run after midnight, it will collect up letters created before 5:30pm the day before.
|
|
|
|
|
"""
|
2020-07-10 11:27:42 +01:00
|
|
|
current_app.logger.info("starting collate-letter-pdfs-to-be-sent")
|
2020-02-19 14:23:33 +00:00
|
|
|
print_run_date = convert_utc_to_bst(datetime.utcnow())
|
|
|
|
|
if print_run_date.time() < LETTER_PROCESSING_DEADLINE:
|
|
|
|
|
print_run_date = print_run_date - timedelta(days=1)
|
|
|
|
|
|
|
|
|
|
print_run_deadline = print_run_date.replace(
|
|
|
|
|
hour=17, minute=30, second=0, microsecond=0
|
|
|
|
|
)
|
2021-02-17 17:47:00 +00:00
|
|
|
_get_letters_and_sheets_volumes_and_send_to_dvla(print_run_deadline)
|
|
|
|
|
|
2020-06-30 17:54:47 +01:00
|
|
|
for postage in POSTAGE_TYPES:
|
2020-07-10 11:27:42 +01:00
|
|
|
current_app.logger.info(f"starting collate-letter-pdfs-to-be-sent processing for postage class {postage}")
|
2020-06-30 17:54:47 +01:00
|
|
|
letters_to_print = get_key_and_size_of_letters_to_be_sent_to_print(print_run_deadline, postage)
|
2020-02-17 15:59:53 +00:00
|
|
|
|
2020-06-30 17:54:47 +01:00
|
|
|
for i, letters in enumerate(group_letters(letters_to_print)):
|
2020-04-06 17:57:16 +01:00
|
|
|
filenames = [letter['Key'] for letter in letters]
|
2021-02-17 17:47:00 +00:00
|
|
|
|
2020-10-20 17:51:35 +01:00
|
|
|
service_id = letters[0]['ServiceId']
|
2020-04-06 17:57:16 +01:00
|
|
|
|
|
|
|
|
hash = urlsafe_b64encode(sha512(''.join(filenames).encode()).digest())[:20].decode()
|
|
|
|
|
# eg NOTIFY.2018-12-31.001.Wjrui5nAvObjPd-3GEL-.ZIP
|
2020-10-20 17:51:35 +01:00
|
|
|
dvla_filename = 'NOTIFY.{date}.{postage}.{num:03}.{hash}.{service_id}.ZIP'.format(
|
2020-04-06 17:57:16 +01:00
|
|
|
date=print_run_deadline.strftime("%Y-%m-%d"),
|
2020-06-30 17:54:47 +01:00
|
|
|
postage=RESOLVE_POSTAGE_FOR_FILE_NAME[postage],
|
|
|
|
|
num=i + 1,
|
2020-10-20 17:51:35 +01:00
|
|
|
hash=hash,
|
|
|
|
|
service_id=service_id
|
2020-04-06 17:57:16 +01:00
|
|
|
)
|
2019-03-21 15:40:24 +00:00
|
|
|
|
2020-04-06 17:57:16 +01:00
|
|
|
current_app.logger.info(
|
|
|
|
|
'Calling task zip-and-send-letter-pdfs for {} pdfs to upload {} with total size {:,} bytes'.format(
|
|
|
|
|
len(filenames),
|
|
|
|
|
dvla_filename,
|
|
|
|
|
sum(letter['Size'] for letter in letters)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
notify_celery.send_task(
|
|
|
|
|
name=TaskNames.ZIP_AND_SEND_LETTER_PDFS,
|
|
|
|
|
kwargs={
|
|
|
|
|
'filenames_to_zip': filenames,
|
|
|
|
|
'upload_filename': dvla_filename
|
|
|
|
|
},
|
|
|
|
|
queue=QueueNames.PROCESS_FTP,
|
|
|
|
|
compression='zlib'
|
2017-12-19 14:18:05 +00:00
|
|
|
)
|
2020-07-10 11:27:42 +01:00
|
|
|
current_app.logger.info(f"finished collate-letter-pdfs-to-be-sent processing for postage class {postage}")
|
|
|
|
|
|
|
|
|
|
current_app.logger.info("finished collate-letter-pdfs-to-be-sent")
|
2017-12-19 14:18:05 +00:00
|
|
|
|
|
|
|
|
|
2021-02-17 17:47:00 +00:00
|
|
|
def _get_letters_and_sheets_volumes_and_send_to_dvla(print_run_deadline):
|
|
|
|
|
letters_volumes = dao_get_letters_and_sheets_volume_by_postage(print_run_deadline)
|
2021-02-23 18:57:36 +00:00
|
|
|
send_letters_volume_email_to_dvla(letters_volumes, print_run_deadline.date())
|
2021-02-17 17:47:00 +00:00
|
|
|
|
|
|
|
|
|
2021-02-23 18:57:36 +00:00
|
|
|
def send_letters_volume_email_to_dvla(letters_volumes, date):
|
2021-02-17 17:47:00 +00:00
|
|
|
personalisation = {
|
|
|
|
|
'total_volume': 0,
|
|
|
|
|
'first_class_volume': 0,
|
|
|
|
|
'second_class_volume': 0,
|
|
|
|
|
'international_volume': 0,
|
|
|
|
|
'total_sheets': 0,
|
|
|
|
|
'first_class_sheets': 0,
|
|
|
|
|
"second_class_sheets": 0,
|
2021-02-23 18:57:36 +00:00
|
|
|
'international_sheets': 0,
|
|
|
|
|
'date': date.strftime("%d %B %Y")
|
2021-02-17 17:47:00 +00:00
|
|
|
}
|
|
|
|
|
for item in letters_volumes:
|
|
|
|
|
personalisation['total_volume'] += item.letters_count
|
|
|
|
|
personalisation['total_sheets'] += item.sheets_count
|
|
|
|
|
if f"{item.postage}_class_volume" in personalisation:
|
|
|
|
|
personalisation[f"{item.postage}_class_volume"] = item.letters_count
|
|
|
|
|
personalisation[f"{item.postage}_class_sheets"] = item.sheets_count
|
|
|
|
|
else:
|
|
|
|
|
personalisation["international_volume"] += item.letters_count
|
|
|
|
|
personalisation["international_sheets"] += item.sheets_count
|
|
|
|
|
|
|
|
|
|
template = dao_get_template_by_id(current_app.config['LETTERS_VOLUME_EMAIL_TEMPLATE_ID'])
|
|
|
|
|
recipient = current_app.config['DVLA_EMAIL_ADDRESS']
|
|
|
|
|
reply_to = template.service.get_default_reply_to_email_address()
|
|
|
|
|
service = Service.query.get(current_app.config['NOTIFY_SERVICE_ID'])
|
|
|
|
|
|
|
|
|
|
# avoid circular imports:
|
|
|
|
|
from app.notifications.process_notifications import (
|
|
|
|
|
persist_notification,
|
|
|
|
|
send_notification_to_queue
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
saved_notification = persist_notification(
|
|
|
|
|
template_id=template.id,
|
|
|
|
|
template_version=template.version,
|
|
|
|
|
recipient=recipient,
|
|
|
|
|
service=service,
|
|
|
|
|
personalisation=personalisation,
|
|
|
|
|
notification_type=template.template_type,
|
|
|
|
|
api_key_id=None,
|
|
|
|
|
key_type=KEY_TYPE_NORMAL,
|
|
|
|
|
reply_to_text=reply_to
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
send_notification_to_queue(saved_notification, False, queue=QueueNames.NOTIFY)
|
|
|
|
|
|
|
|
|
|
|
2020-06-30 17:54:47 +01:00
|
|
|
def get_key_and_size_of_letters_to_be_sent_to_print(print_run_deadline, postage):
|
|
|
|
|
letters_awaiting_sending = dao_get_letters_to_be_printed(print_run_deadline, postage)
|
2020-02-17 15:59:53 +00:00
|
|
|
for letter in letters_awaiting_sending:
|
2020-03-19 09:15:38 +00:00
|
|
|
try:
|
|
|
|
|
letter_file_name = get_letter_pdf_filename(
|
|
|
|
|
reference=letter.reference,
|
2020-10-23 20:01:16 +01:00
|
|
|
crown=letter.crown,
|
2020-09-21 14:40:22 +01:00
|
|
|
created_at=letter.created_at,
|
2020-10-23 20:01:16 +01:00
|
|
|
postage=postage
|
2020-03-19 09:15:38 +00:00
|
|
|
)
|
|
|
|
|
letter_head = s3.head_s3_object(current_app.config['LETTERS_PDF_BUCKET_NAME'], letter_file_name)
|
2020-10-23 10:21:52 +01:00
|
|
|
yield {
|
2020-10-20 17:51:35 +01:00
|
|
|
"Key": letter_file_name,
|
|
|
|
|
"Size": letter_head['ContentLength'],
|
2020-10-23 20:01:16 +01:00
|
|
|
"ServiceId": str(letter.service_id)
|
2020-10-23 10:21:52 +01:00
|
|
|
}
|
2020-03-19 09:15:38 +00:00
|
|
|
except BotoClientError as e:
|
|
|
|
|
current_app.logger.exception(
|
|
|
|
|
f"Error getting letter from bucket for notification: {letter.id} with reference: {letter.reference}", e)
|
2020-02-17 15:59:53 +00:00
|
|
|
|
|
|
|
|
|
2017-12-19 14:18:05 +00:00
|
|
|
def group_letters(letter_pdfs):
|
|
|
|
|
"""
|
|
|
|
|
Group letters in chunks of MAX_LETTER_PDF_ZIP_FILESIZE. Will add files to lists, never going over that size.
|
|
|
|
|
If a single file is (somehow) larger than MAX_LETTER_PDF_ZIP_FILESIZE that'll be in a list on it's own.
|
|
|
|
|
If there are no files, will just exit (rather than yielding an empty list).
|
|
|
|
|
"""
|
|
|
|
|
running_filesize = 0
|
|
|
|
|
list_of_files = []
|
2020-10-20 17:51:35 +01:00
|
|
|
service_id = None
|
2017-12-19 14:18:05 +00:00
|
|
|
for letter in letter_pdfs:
|
2020-02-19 12:54:06 +00:00
|
|
|
if letter['Key'].lower().endswith('.pdf'):
|
2020-10-20 17:51:35 +01:00
|
|
|
if not service_id:
|
|
|
|
|
service_id = letter['ServiceId']
|
2018-01-02 17:18:01 +00:00
|
|
|
if (
|
2020-10-20 17:51:35 +01:00
|
|
|
running_filesize + letter['Size'] > current_app.config['MAX_LETTER_PDF_ZIP_FILESIZE']
|
|
|
|
|
or len(list_of_files) >= current_app.config['MAX_LETTER_PDF_COUNT_PER_ZIP']
|
|
|
|
|
or letter['ServiceId'] != service_id
|
2018-01-02 17:18:01 +00:00
|
|
|
):
|
2017-12-22 15:38:49 +00:00
|
|
|
yield list_of_files
|
|
|
|
|
running_filesize = 0
|
|
|
|
|
list_of_files = []
|
2020-10-20 17:51:35 +01:00
|
|
|
service_id = None
|
2017-12-22 15:38:49 +00:00
|
|
|
|
2020-10-20 17:51:35 +01:00
|
|
|
if not service_id:
|
|
|
|
|
service_id = letter['ServiceId']
|
2017-12-22 15:38:49 +00:00
|
|
|
running_filesize += letter['Size']
|
|
|
|
|
list_of_files.append(letter)
|
2017-12-19 14:18:05 +00:00
|
|
|
|
|
|
|
|
if list_of_files:
|
|
|
|
|
yield list_of_files
|
2018-01-15 17:00:00 +00:00
|
|
|
|
|
|
|
|
|
2019-12-04 16:02:46 +00:00
|
|
|
@notify_celery.task(bind=True, name='sanitise-letter', max_retries=15, default_retry_delay=300)
|
|
|
|
|
def sanitise_letter(self, filename):
|
|
|
|
|
try:
|
|
|
|
|
reference = get_reference_from_filename(filename)
|
|
|
|
|
notification = dao_get_notification_by_reference(reference)
|
|
|
|
|
|
|
|
|
|
current_app.logger.info('Notification ID {} Virus scan passed: {}'.format(notification.id, filename))
|
|
|
|
|
|
|
|
|
|
if notification.status != NOTIFICATION_PENDING_VIRUS_CHECK:
|
2020-03-27 14:28:58 +00:00
|
|
|
current_app.logger.info('Sanitise letter called for notification {} which is in {} state'.format(
|
2019-12-04 16:02:46 +00:00
|
|
|
notification.id, notification.status))
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
notify_celery.send_task(
|
|
|
|
|
name=TaskNames.SANITISE_LETTER,
|
|
|
|
|
kwargs={
|
|
|
|
|
'notification_id': str(notification.id),
|
|
|
|
|
'filename': filename,
|
2020-05-01 14:26:20 +01:00
|
|
|
'allow_international_letters': notification.service.has_permission(
|
|
|
|
|
INTERNATIONAL_LETTERS
|
|
|
|
|
),
|
2019-12-04 16:02:46 +00:00
|
|
|
},
|
|
|
|
|
queue=QueueNames.SANITISE_LETTERS,
|
|
|
|
|
)
|
|
|
|
|
except Exception:
|
|
|
|
|
try:
|
|
|
|
|
current_app.logger.exception(
|
|
|
|
|
"RETRY: calling sanitise_letter task for notification {} failed".format(notification.id)
|
|
|
|
|
)
|
|
|
|
|
self.retry(queue=QueueNames.RETRY)
|
|
|
|
|
except self.MaxRetriesExceededError:
|
|
|
|
|
message = "RETRY FAILED: Max retries reached. " \
|
|
|
|
|
"The task sanitise_letter failed for notification {}. " \
|
|
|
|
|
"Notification has been updated to technical-failure".format(notification.id)
|
|
|
|
|
update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE)
|
|
|
|
|
raise NotificationTechnicalFailureException(message)
|
|
|
|
|
|
|
|
|
|
|
2020-03-27 14:28:58 +00:00
|
|
|
@notify_celery.task(bind=True, name='process-sanitised-letter', max_retries=15, default_retry_delay=300)
|
|
|
|
|
def process_sanitised_letter(self, sanitise_data):
|
2020-01-24 09:08:27 +00:00
|
|
|
letter_details = encryption.decrypt(sanitise_data)
|
|
|
|
|
|
|
|
|
|
filename = letter_details['filename']
|
|
|
|
|
notification_id = letter_details['notification_id']
|
|
|
|
|
|
2019-12-04 16:02:46 +00:00
|
|
|
current_app.logger.info('Processing sanitised letter with id {}'.format(notification_id))
|
|
|
|
|
notification = get_notification_by_id(notification_id, _raise=True)
|
|
|
|
|
|
|
|
|
|
if notification.status != NOTIFICATION_PENDING_VIRUS_CHECK:
|
|
|
|
|
current_app.logger.info(
|
2020-03-27 14:28:58 +00:00
|
|
|
'process-sanitised-letter task called for notification {} which is in {} state'.format(
|
2019-12-04 16:02:46 +00:00
|
|
|
notification.id, notification.status)
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
original_pdf_object = s3.get_s3_object(current_app.config['LETTERS_SCAN_BUCKET_NAME'], filename)
|
|
|
|
|
|
2020-01-24 09:08:27 +00:00
|
|
|
if letter_details['validation_status'] == 'failed':
|
2019-12-04 16:02:46 +00:00
|
|
|
current_app.logger.info('Processing invalid precompiled pdf with id {} (file {})'.format(
|
|
|
|
|
notification_id, filename))
|
|
|
|
|
|
|
|
|
|
_move_invalid_letter_and_update_status(
|
|
|
|
|
notification=notification,
|
|
|
|
|
filename=filename,
|
|
|
|
|
scan_pdf_object=original_pdf_object,
|
2020-01-24 09:08:27 +00:00
|
|
|
message=letter_details['message'],
|
|
|
|
|
invalid_pages=letter_details['invalid_pages'],
|
|
|
|
|
page_count=letter_details['page_count'],
|
2019-12-04 16:02:46 +00:00
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
current_app.logger.info('Processing valid precompiled pdf with id {} (file {})'.format(
|
|
|
|
|
notification_id, filename))
|
|
|
|
|
|
2020-01-24 09:08:27 +00:00
|
|
|
billable_units = get_billable_units_for_letter_page_count(letter_details['page_count'])
|
2019-12-04 16:02:46 +00:00
|
|
|
is_test_key = notification.key_type == KEY_TYPE_TEST
|
|
|
|
|
|
2020-03-27 14:28:58 +00:00
|
|
|
# Updating the notification needs to happen before the file is moved. This is so that if updating the
|
|
|
|
|
# notification fails, the task can retry because the file is in the same place.
|
2019-12-04 16:02:46 +00:00
|
|
|
update_letter_pdf_status(
|
|
|
|
|
reference=notification.reference,
|
|
|
|
|
status=NOTIFICATION_DELIVERED if is_test_key else NOTIFICATION_CREATED,
|
2020-01-24 10:15:19 +00:00
|
|
|
billable_units=billable_units,
|
|
|
|
|
recipient_address=letter_details['address']
|
2019-12-04 16:02:46 +00:00
|
|
|
)
|
2020-09-15 16:17:33 +01:00
|
|
|
|
|
|
|
|
# The original filename could be wrong because we didn't know the postage.
|
|
|
|
|
# Now we know if the letter is international, we can check what the filename should be.
|
|
|
|
|
upload_file_name = get_letter_pdf_filename(
|
|
|
|
|
reference=notification.reference,
|
|
|
|
|
crown=notification.service.crown,
|
2020-09-21 14:40:22 +01:00
|
|
|
created_at=notification.created_at,
|
2020-09-21 13:46:31 +01:00
|
|
|
ignore_folder=True,
|
2020-09-15 16:17:33 +01:00
|
|
|
postage=notification.postage
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
move_sanitised_letter_to_test_or_live_pdf_bucket(
|
|
|
|
|
filename,
|
|
|
|
|
is_test_key,
|
|
|
|
|
notification.created_at,
|
|
|
|
|
upload_file_name,
|
|
|
|
|
)
|
2020-03-27 14:28:58 +00:00
|
|
|
# We've moved the sanitised PDF from the sanitise bucket, but still need to delete the original file:
|
|
|
|
|
original_pdf_object.delete()
|
2019-12-04 16:02:46 +00:00
|
|
|
|
|
|
|
|
except BotoClientError:
|
2020-03-27 14:28:58 +00:00
|
|
|
# Boto exceptions are likely to be caused by the file(s) being in the wrong place, so retrying won't help -
|
|
|
|
|
# we'll need to manually investigate
|
2019-12-04 16:02:46 +00:00
|
|
|
current_app.logger.exception(
|
2020-03-27 14:28:58 +00:00
|
|
|
f"Boto error when processing sanitised letter for notification {notification.id} (file {filename})"
|
2019-12-04 16:02:46 +00:00
|
|
|
)
|
|
|
|
|
update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE)
|
|
|
|
|
raise NotificationTechnicalFailureException
|
2020-03-27 14:28:58 +00:00
|
|
|
except Exception:
|
|
|
|
|
try:
|
|
|
|
|
current_app.logger.exception(
|
|
|
|
|
"RETRY: calling process_sanitised_letter task for notification {} failed".format(notification.id)
|
|
|
|
|
)
|
|
|
|
|
self.retry(queue=QueueNames.RETRY)
|
|
|
|
|
except self.MaxRetriesExceededError:
|
|
|
|
|
message = "RETRY FAILED: Max retries reached. " \
|
|
|
|
|
"The task process_sanitised_letter failed for notification {}. " \
|
|
|
|
|
"Notification has been updated to technical-failure".format(notification.id)
|
|
|
|
|
update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE)
|
|
|
|
|
raise NotificationTechnicalFailureException(message)
|
2019-12-04 16:02:46 +00:00
|
|
|
|
|
|
|
|
|
2019-10-11 12:24:53 +01:00
|
|
|
def _move_invalid_letter_and_update_status(
|
|
|
|
|
*, notification, filename, scan_pdf_object, message=None, invalid_pages=None, page_count=None
|
|
|
|
|
):
|
2019-06-11 11:00:04 +01:00
|
|
|
try:
|
2019-10-11 12:24:53 +01:00
|
|
|
move_scan_to_invalid_pdf_bucket(
|
|
|
|
|
source_filename=filename,
|
|
|
|
|
message=message,
|
|
|
|
|
invalid_pages=invalid_pages,
|
|
|
|
|
page_count=page_count
|
|
|
|
|
)
|
2019-06-11 11:00:04 +01:00
|
|
|
scan_pdf_object.delete()
|
|
|
|
|
|
|
|
|
|
update_letter_pdf_status(
|
|
|
|
|
reference=notification.reference,
|
|
|
|
|
status=NOTIFICATION_VALIDATION_FAILED,
|
|
|
|
|
billable_units=0)
|
|
|
|
|
except BotoClientError:
|
|
|
|
|
current_app.logger.exception(
|
|
|
|
|
"Error when moving letter with id {} to invalid PDF bucket".format(notification.id)
|
|
|
|
|
)
|
|
|
|
|
update_notification_status_by_id(notification.id, NOTIFICATION_TECHNICAL_FAILURE)
|
2019-12-04 16:02:46 +00:00
|
|
|
raise NotificationTechnicalFailureException
|
2019-01-11 09:23:05 +00:00
|
|
|
|
|
|
|
|
|
2018-03-20 14:56:42 +00:00
|
|
|
@notify_celery.task(name='process-virus-scan-failed')
|
|
|
|
|
def process_virus_scan_failed(filename):
|
2018-03-23 15:27:24 +00:00
|
|
|
move_failed_pdf(filename, ScanErrorType.FAILURE)
|
2018-03-19 13:52:01 +00:00
|
|
|
reference = get_reference_from_filename(filename)
|
2018-04-03 12:31:52 +01:00
|
|
|
notification = dao_get_notification_by_reference(reference)
|
2018-10-24 14:50:50 +01:00
|
|
|
updated_count = update_letter_pdf_status(reference, NOTIFICATION_VIRUS_SCAN_FAILED, billable_units=0)
|
2018-03-19 13:52:01 +00:00
|
|
|
|
|
|
|
|
if updated_count != 1:
|
|
|
|
|
raise Exception(
|
|
|
|
|
"There should only be one letter notification for each reference. Found {} notifications".format(
|
|
|
|
|
updated_count
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
2018-06-27 16:40:30 +01:00
|
|
|
error = VirusScanError('notification id {} Virus scan failed: {}'.format(notification.id, filename))
|
|
|
|
|
current_app.logger.exception(error)
|
|
|
|
|
raise error
|
2018-04-03 12:31:52 +01:00
|
|
|
|
2018-03-19 13:52:01 +00:00
|
|
|
|
2018-03-23 15:27:24 +00:00
|
|
|
@notify_celery.task(name='process-virus-scan-error')
|
|
|
|
|
def process_virus_scan_error(filename):
|
|
|
|
|
move_failed_pdf(filename, ScanErrorType.ERROR)
|
|
|
|
|
reference = get_reference_from_filename(filename)
|
2018-04-03 12:31:52 +01:00
|
|
|
notification = dao_get_notification_by_reference(reference)
|
2018-10-24 14:50:50 +01:00
|
|
|
updated_count = update_letter_pdf_status(reference, NOTIFICATION_TECHNICAL_FAILURE, billable_units=0)
|
2018-03-23 15:27:24 +00:00
|
|
|
|
|
|
|
|
if updated_count != 1:
|
|
|
|
|
raise Exception(
|
|
|
|
|
"There should only be one letter notification for each reference. Found {} notifications".format(
|
|
|
|
|
updated_count
|
|
|
|
|
)
|
|
|
|
|
)
|
2018-06-27 16:40:30 +01:00
|
|
|
error = VirusScanError('notification id {} Virus scan error: {}'.format(notification.id, filename))
|
|
|
|
|
current_app.logger.exception(error)
|
|
|
|
|
raise error
|
2018-04-03 12:31:52 +01:00
|
|
|
|
2018-03-23 15:27:24 +00:00
|
|
|
|
2020-01-01 17:18:52 +00:00
|
|
|
def update_letter_pdf_status(reference, status, billable_units, recipient_address=None):
|
2020-07-29 14:52:18 +01:00
|
|
|
postage = None
|
|
|
|
|
if recipient_address:
|
|
|
|
|
# fix allow_international_letters
|
|
|
|
|
postage = PostalAddress(raw_address=recipient_address.replace(',', '\n'),
|
|
|
|
|
allow_international_letters=True
|
|
|
|
|
).postage
|
|
|
|
|
postage = postage if postage in INTERNATIONAL_POSTAGE_TYPES else None
|
2020-01-01 17:18:52 +00:00
|
|
|
update_dict = {'status': status, 'billable_units': billable_units, 'updated_at': datetime.utcnow()}
|
2020-07-29 14:52:18 +01:00
|
|
|
if postage:
|
|
|
|
|
update_dict.update({'postage': postage, 'international': True})
|
2020-01-01 17:18:52 +00:00
|
|
|
if recipient_address:
|
|
|
|
|
update_dict['to'] = recipient_address
|
2018-03-19 13:52:01 +00:00
|
|
|
return dao_update_notifications_by_reference(
|
|
|
|
|
references=[reference],
|
2020-01-01 17:18:52 +00:00
|
|
|
update_dict=update_dict)[0]
|
2018-06-27 16:40:30 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def replay_letters_in_error(filename=None):
|
|
|
|
|
# This method can be used to replay letters that end up in the ERROR directory.
|
|
|
|
|
# We had an incident where clamAV was not processing the virus scan.
|
|
|
|
|
if filename:
|
|
|
|
|
move_error_pdf_to_scan_bucket(filename)
|
|
|
|
|
# call task to add the filename to anti virus queue
|
|
|
|
|
current_app.logger.info("Calling scan_file for: {}".format(filename))
|
2019-02-25 14:26:47 +00:00
|
|
|
|
|
|
|
|
if current_app.config['ANTIVIRUS_ENABLED']:
|
|
|
|
|
notify_celery.send_task(
|
|
|
|
|
name=TaskNames.SCAN_FILE,
|
|
|
|
|
kwargs={'filename': filename},
|
|
|
|
|
queue=QueueNames.ANTIVIRUS,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# stub out antivirus in dev
|
2020-03-10 15:15:58 +00:00
|
|
|
sanitise_letter.apply_async(
|
|
|
|
|
[filename],
|
|
|
|
|
queue=QueueNames.LETTERS
|
2019-02-25 14:26:47 +00:00
|
|
|
)
|
2018-06-27 16:40:30 +01:00
|
|
|
else:
|
|
|
|
|
error_files = get_file_names_from_error_bucket()
|
|
|
|
|
for item in error_files:
|
|
|
|
|
moved_file_name = item.key.split('/')[1]
|
|
|
|
|
current_app.logger.info("Calling scan_file for: {}".format(moved_file_name))
|
|
|
|
|
move_error_pdf_to_scan_bucket(moved_file_name)
|
|
|
|
|
# call task to add the filename to anti virus queue
|
2019-02-25 14:26:47 +00:00
|
|
|
if current_app.config['ANTIVIRUS_ENABLED']:
|
|
|
|
|
notify_celery.send_task(
|
|
|
|
|
name=TaskNames.SCAN_FILE,
|
|
|
|
|
kwargs={'filename': moved_file_name},
|
|
|
|
|
queue=QueueNames.ANTIVIRUS,
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
# stub out antivirus in dev
|
2020-03-10 15:15:58 +00:00
|
|
|
sanitise_letter.apply_async(
|
|
|
|
|
[filename],
|
|
|
|
|
queue=QueueNames.LETTERS
|
2019-02-25 14:26:47 +00:00
|
|
|
)
|