1- import base64
21import contextlib
32import secrets
43import string
@@ -254,6 +253,36 @@ def _get_auth_headers(self):
254253 "X-API-Key" : self .dvla_api_key .get (),
255254 }
256255
256+ def _get_upload_url (self ):
257+ """
258+ Calls the DVLA endpoint to get a presigned URL to use to upload a letter
259+ """
260+
261+ def _handle_http_errors (e : requests .HTTPError ):
262+ if e .response .status_code == 400 :
263+ raise DvlaNonRetryableException (e .response .json ()["errors" ][0 ]["detail" ]) from e
264+ elif e .response .status_code in {401 , 403 }:
265+ # probably the api key is not valid
266+ self .dvla_api_key .clear ()
267+
268+ raise DvlaUnauthorisedRequestException (e .response .json ()["errors" ][0 ]["detail" ]) from e
269+
270+ with _handle_common_dvla_errors (custom_httperror_exc_handler = _handle_http_errors ):
271+ response = self .session .get (
272+ f"{ self .base_url } /print-request/v1/print/files/upload-url" ,
273+ headers = self ._get_auth_headers (),
274+ )
275+ response .raise_for_status ()
276+ return response .json ()
277+
278+ def _upload_file (self , * , upload_url : str , pdf_file : bytes ):
279+ """
280+ Uploads the letter to the given presigned URL
281+ """
282+ with _handle_common_dvla_errors ():
283+ response = self .session .put (upload_url , headers = {"Content-Type" : "application/pdf" }, data = pdf_file )
284+ response .raise_for_status ()
285+
257286 def send_letter (
258287 self ,
259288 * ,
@@ -269,6 +298,16 @@ def send_letter(
269298 """
270299 Sends a letter to the DVLA for printing
271300 """
301+ url_response = self ._get_upload_url ()
302+ upload_id = url_response ["uploadId" ]
303+ upload_url = url_response ["uploadUrl" ]
304+
305+ self ._upload_file (upload_url = upload_url , pdf_file = pdf_file )
306+ current_app .logger .info (
307+ "Letter with notification id %s uploaded to DVLA presigned URL" ,
308+ notification_id ,
309+ extra = {"notification_id" : notification_id },
310+ )
272311
273312 def _handle_http_errors (e : requests .HTTPError ):
274313 if e .response .status_code == 400 :
@@ -292,15 +331,15 @@ def _handle_http_errors(e: requests.HTTPError):
292331 postage = postage ,
293332 service_id = service_id ,
294333 organisation_id = organisation_id ,
295- pdf_file = pdf_file ,
334+ upload_id = upload_id ,
296335 callback_url = callback_url ,
297336 ),
298337 )
299338 response .raise_for_status ()
300339 return response .json ()
301340
302341 def _format_create_print_job_json (
303- self , * , notification_id , reference , address , postage , service_id , organisation_id , pdf_file , callback_url
342+ self , * , notification_id , reference , address , postage , service_id , organisation_id , upload_id , callback_url
304343 ):
305344 # We shouldn't need to pass the postage in, as the address has a postage field. However, at this point we've
306345 # recorded the postage on the notification so we should respect that rather than introduce any possible
@@ -320,7 +359,6 @@ def _format_create_print_job_json(
320359 "address" : address_data ,
321360 },
322361 "customParams" : [
323- {"key" : "pdfContent" , "value" : base64 .b64encode (pdf_file ).decode ("utf-8" )},
324362 {"key" : "organisationIdentifier" , "value" : organisation_id },
325363 {"key" : "serviceIdentifier" , "value" : service_id },
326364 ],
@@ -331,6 +369,8 @@ def _format_create_print_job_json(
331369 "retryParams" : {"enabled" : True , "maxRetryWindow" : 10800 },
332370 }
333371
372+ json_payload ["fileParams" ] = [{"fileId" : notification_id , "uploadId" : upload_id }]
373+
334374 # `despatchMethod` should not be added for second class letters
335375 if postage == FIRST_CLASS :
336376 json_payload ["standardParams" ]["despatchMethod" ] = "FIRST"
0 commit comments