Skip to content
Draft

WIP #4776

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 69 additions & 18 deletions app/clients/letter/dvla.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import base64
import contextlib
import secrets
import string
Expand All @@ -9,6 +8,7 @@
import boto3
import jwt
import requests
import sentry_sdk
from flask import current_app
from notifications_utils.recipient_validation.postal_address import PostalAddress
from requests.adapters import HTTPAdapter
Expand Down Expand Up @@ -254,6 +254,43 @@ def _get_auth_headers(self):
"X-API-Key": self.dvla_api_key.get(),
}

def _get_upload_url(self):
"""
Calls the DVLA endpoint to get a presigned URL to use to upload a letter.
URLs are valid for 1 hour.
"""

def _handle_http_errors(e: requests.HTTPError):
if e.response.status_code == 400:
raise DvlaNonRetryableException(e.response.json()["errors"][0]["detail"]) from e
elif e.response.status_code in {401, 403}:
# probably the api key is not valid
self.dvla_api_key.clear()

raise DvlaUnauthorisedRequestException(e.response.json()["errors"][0]["detail"]) from e

with _handle_common_dvla_errors(custom_httperror_exc_handler=_handle_http_errors):
response = self.session.get(
f"{self.base_url}/print-request/v1/print/files/upload-url",
headers=self._get_auth_headers(),
)
response.raise_for_status()
return response.json()

def _upload_file(self, *, upload_url: str, pdf_file: bytes):
"""
Uploads the letter to DVLA's S3 account using the given presigned URL.

While the endpoint isn't a DVLA one, we still want the error handling behaviour of `_handle_common_dvla_errors`.

Being rate limited when uploading the file will give a `503` status code instead of a `429` code. However, this
isn't handled separately because we should hit DVLA rate limits before being rate limited here, and 5xx status
codes are retried.
"""
with _handle_common_dvla_errors():
response = self.session.put(upload_url, headers={"Content-Type": "application/pdf"}, data=pdf_file)
response.raise_for_status()

def send_letter(
self,
*,
Expand All @@ -269,6 +306,18 @@ def send_letter(
"""
Sends a letter to the DVLA for printing
"""
with sentry_sdk.start_span(op="http", description="DVLA GET upload URL"):
url_response = self._get_upload_url()
upload_id = url_response["uploadId"]
upload_url = url_response["uploadUrl"]

with sentry_sdk.start_span(op="http", description="DVLA PUT upload PDF"):
self._upload_file(upload_url=upload_url, pdf_file=pdf_file)
current_app.logger.info(
"Letter with notification id %s uploaded to DVLA presigned URL",
notification_id,
extra={"notification_id": notification_id},
)

def _handle_http_errors(e: requests.HTTPError):
if e.response.status_code == 400:
Expand All @@ -282,25 +331,26 @@ def _handle_http_errors(e: requests.HTTPError):
raise DvlaDuplicatePrintRequestException(e.response.json()["errors"][0]["detail"]) from e

with _handle_common_dvla_errors(custom_httperror_exc_handler=_handle_http_errors):
response = self.session.post(
f"{self.base_url}/print-request/v1/print/jobs",
headers=self._get_auth_headers(),
json=self._format_create_print_job_json(
notification_id=notification_id,
reference=reference,
address=address,
postage=postage,
service_id=service_id,
organisation_id=organisation_id,
pdf_file=pdf_file,
callback_url=callback_url,
),
)
response.raise_for_status()
with sentry_sdk.start_span(op="http", description="DVLA POST create letter"):
response = self.session.post(
f"{self.base_url}/print-request/v1/print/jobs",
headers=self._get_auth_headers(),
json=self._format_create_print_job_json(
notification_id=notification_id,
reference=reference,
address=address,
postage=postage,
service_id=service_id,
organisation_id=organisation_id,
upload_id=upload_id,
callback_url=callback_url,
),
)
response.raise_for_status()
return response.json()

def _format_create_print_job_json(
self, *, notification_id, reference, address, postage, service_id, organisation_id, pdf_file, callback_url
self, *, notification_id, reference, address, postage, service_id, organisation_id, upload_id, callback_url
):
# We shouldn't need to pass the postage in, as the address has a postage field. However, at this point we've
# recorded the postage on the notification so we should respect that rather than introduce any possible
Expand All @@ -320,7 +370,6 @@ def _format_create_print_job_json(
"address": address_data,
},
"customParams": [
{"key": "pdfContent", "value": base64.b64encode(pdf_file).decode("utf-8")},
{"key": "organisationIdentifier", "value": organisation_id},
{"key": "serviceIdentifier", "value": service_id},
],
Expand All @@ -331,6 +380,8 @@ def _format_create_print_job_json(
"retryParams": {"enabled": True, "maxRetryWindow": 10800},
}

json_payload["fileParams"] = [{"fileId": notification_id, "uploadId": upload_id}]

# `despatchMethod` should not be added for second class letters
if postage == FIRST_CLASS:
json_payload["standardParams"]["despatchMethod"] = "FIRST"
Expand Down
Loading
Loading