diff --git a/legal-api/src/legal_api/config.py b/legal-api/src/legal_api/config.py index 0b35323e3a..2bde6f1aa7 100644 --- a/legal-api/src/legal_api/config.py +++ b/legal-api/src/legal_api/config.py @@ -182,6 +182,11 @@ class _Config(): # pylint: disable=too-few-public-methods STAGE_1_DELAY = int(os.getenv('STAGE_1_DELAY', '42')) STAGE_2_DELAY = int(os.getenv('STAGE_2_DELAY', '30')) + # Document Record Service Settings + DRS_BASE_URL = os.getenv('DRS_BASE_URL', '') + DRS_ACCOUNT_ID = os.getenv('DRS_ACCOUNT_ID', '') + DRS_X_API_KEY = os.getenv('DRS_X_API_KEY', '') + TESTING = False DEBUG = False diff --git a/legal-api/src/legal_api/constants.py b/legal-api/src/legal_api/constants.py index 92e7394c79..403cc32e13 100644 --- a/legal-api/src/legal_api/constants.py +++ b/legal-api/src/legal_api/constants.py @@ -13,4 +13,14 @@ # limitations under the License. """Constants for legal api.""" +from enum import Enum + + BOB_DATE = '2019-03-08' + +class DocumentClassEnum(Enum): + CORP = 'CORP' + +class DocumentTypeEnum(Enum): + CNTO = 'CNTO', + DIRECTOR_AFFIDAVIT = 'DIRECTOR_AFFIDAVIT' \ No newline at end of file diff --git a/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py b/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py index 5b62e18a3e..ddee00159c 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py +++ b/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py @@ -26,7 +26,7 @@ from legal_api.exceptions import ErrorCode, get_error_message from legal_api.models import Business, Document, Filing as FilingModel # noqa: I001 from legal_api.reports import get_pdf -from legal_api.services import MinioService, authorized +from legal_api.services import MinioService, authorized, DocumentRecordService from legal_api.utils.auth import jwt from legal_api.utils.legislation_datetime import LegislationDatetime from legal_api.utils.util import cors_preflight @@ -82,6 +82,13 @@ def get_documents(identifier: str, filing_id: int, legal_filing_name: str = None return get_pdf(filing.storage, legal_filing_name) elif file_key and (document := Document.find_by_file_key(file_key)): if document.filing_id == filing.id: # make sure the file belongs to this filing + if document.file_key.startswith('DS'): # docID from DRS + response = DocumentRecordService.download_document('CORP', document.file_key) + return current_app.response_class( + response=response, + status=HTTPStatus.OK, + mimetype='application/pdf' + ) response = MinioService.get_file(document.file_key) return current_app.response_class( response=response.data, diff --git a/legal-api/src/legal_api/resources/v2/document.py b/legal-api/src/legal_api/resources/v2/document.py index 8d08f9af91..ea21ccd9ef 100644 --- a/legal-api/src/legal_api/resources/v2/document.py +++ b/legal-api/src/legal_api/resources/v2/document.py @@ -20,6 +20,7 @@ from legal_api.models import Document, Filing from legal_api.services.minio import MinioService +from legal_api.services.document_record import DocumentRecordService from legal_api.utils.auth import jwt @@ -77,3 +78,27 @@ def get_minio_document(document_key: str): return jsonify( message=f'Error getting file {document_key}.' ), HTTPStatus.INTERNAL_SERVER_ERROR + +@bp.route('//', methods=['POST', 'OPTIONS']) +@cross_origin(origin='*') +@jwt.requires_auth +def upload_document(document_class: str, document_type: str): + """Upload document file to Document Record Service.""" + + return DocumentRecordService.upload_document(document_class, document_type), HTTPStatus.OK + +@bp.route('/drs/', methods=['DELETE']) +@cross_origin(origin='*') +@jwt.requires_auth +def delete_document(document_service_id: str): + """Delete document file from Document Record Service.""" + + return DocumentRecordService.delete_document(document_service_id), HTTPStatus.OK + +@bp.route('/drs//', methods=['GET']) +@cross_origin(origins='*') +@jwt.requires_auth +def get_document(document_class: str, document_service_id: str): + """Get document file from Document Record Service.""" + + return DocumentRecordService.get_document(document_class, document_service_id), HTTPStatus.OK \ No newline at end of file diff --git a/legal-api/src/legal_api/services/__init__.py b/legal-api/src/legal_api/services/__init__.py index c2a44e2dc0..7f895c28a1 100644 --- a/legal-api/src/legal_api/services/__init__.py +++ b/legal-api/src/legal_api/services/__init__.py @@ -29,6 +29,7 @@ from .furnishing_documents_service import FurnishingDocumentsService from .involuntary_dissolution import InvoluntaryDissolutionService from .minio import MinioService +from .document_record import DocumentRecordService from .mras_service import MrasService from .naics import NaicsService from .namex import NameXService diff --git a/legal-api/src/legal_api/services/document_record.py b/legal-api/src/legal_api/services/document_record.py new file mode 100644 index 0000000000..b373181e37 --- /dev/null +++ b/legal-api/src/legal_api/services/document_record.py @@ -0,0 +1,177 @@ +# Copyright © 2021 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an 'AS IS' BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This module is a wrapper for Document Record Service.""" + +import base64 +from typing import Optional +import requests +from flask import current_app, request +from flask_babel import _ +import PyPDF2 + +from legal_api.constants import DocumentTypeEnum + +class DocumentRecordService: + """Document Storage class.""" + + + @staticmethod + def upload_document(document_class: str, document_type: str) -> dict: + """Upload document to Docuemtn Record Service.""" + query_params = request.args.to_dict() + file = request.data.get('file') + # Ensure file exists + if not file: + current_app.logger.debug('No file found in request.') + return {'data': 'File not provided'} + current_app.logger.debug(f'Upload file to document record service {file.filename}') + DRS_BASE_URL = current_app.config.get('DRS_BASE_URL', '') # pylint: disable=invalid-name + url = f'{DRS_BASE_URL}/documents/{document_class}/{document_type}' + + # Validate file size and encryption status before submitting to DRS. + validation_error = DocumentRecordService.validate_pdf(file, request.content_length, document_type) + if validation_error: + return { + 'error': validation_error + } + + try: + # Read and encode the file content as base64 + file_content = file.read() + file_base64 = base64.b64encode(file_content).decode('utf-8') + + response_body = requests.post( + url, + params=query_params, + json={ + 'filename': file.filename, + 'content': file_base64, + 'content_type': file.content_type, + }, + headers={ + 'x-apikey': current_app.config.get('DRS_X_API_KEY', ''), + 'Account-Id': current_app.config.get('DRS_ACCOUNT_ID', ''), + 'Content-Type': file.content_type + } + ).json() + + current_app.logger.debug(f'Upload file to document record service {response_body}') + return { + 'documentServiceId': response_body['documentServiceId'], + 'consumerDocumentId': response_body['consumerDocumentId'], + 'consumerFilename': response_body['consumerFilename'] + } + except Exception as e: + current_app.logger.debug(f"Error on uploading document {e}") + return {} + + @staticmethod + def delete_document(document_service_id: str) -> dict: + """Delete document from Document Record Service.""" + DRS_BASE_URL = current_app.config.get('DRS_BASE_URL', '') # pylint: disable=invalid-name + url = f'{DRS_BASE_URL}/documents/{document_service_id}' + + try: + response = requests.patch( + url, json={ 'removed': True }, + headers={ + 'x-apikey': current_app.config.get('DRS_X_API_KEY', ''), + 'Account-Id': current_app.config.get('DRS_ACCOUNT_ID', ''), + } + ).json() + current_app.logger.debug(f'Delete document from document record service {response}') + return response + except Exception as e: + current_app.logger.debug(f'Error on deleting document {e}') + return {} + + @staticmethod + def get_document(document_class: str, document_service_id: str) -> dict: + """Get document record from Document Record Service.""" + DRS_BASE_URL = current_app.config.get('DRS_BASE_URL', '') # pylint: disable=invalid-name + url = f'{DRS_BASE_URL}/searches/{document_class}?documentServiceId={document_service_id}' + try: + response = requests.get( + url, + headers={ + 'x-apikey': current_app.config.get('DRS_X_API_KEY', ''), + 'Account-Id': current_app.config.get('DRS_ACCOUNT_ID', ''), + } + ).json() + current_app.logger.debug(f'Get document from document record service {response}') + return response[0] + except Exception as e: + current_app.logger.debug(f'Error on getting a document object {e}') + return {} + + @staticmethod + def download_document(document_class: str, document_service_id: str) -> dict: + """Download document from Document Record Service.""" + doc_object = DocumentRecordService.get_document(document_class, document_service_id) + + response = requests.get(doc_object['documentURL']) # Download file from storage + response.raise_for_status() # Raise an HTTPError for bad responses (4xx and 5xx) + + return response + + @staticmethod + def update_business_identifier(business_identifier: str, document_service_id: str): + """Update business identifier up on approval.""" + DRS_BASE_URL = current_app.config.get('DRS_BASE_URL', '') # pylint: disable=invalid-name + url = f'{DRS_BASE_URL}/documents/{document_service_id}' + + try: + response = requests.patch( + url, json={ 'consumerIdentifer': business_identifier }, + headers={ + 'x-apikey': current_app.config.get('DRS_X_API_KEY', ''), + 'Account-Id': current_app.config.get('DRS_ACCOUNT_ID', ''), + } + ).json() + current_app.logger.debug(f'Update business identifier - {business_identifier}') + return response + except Exception as e: + current_app.logger.debug(f'Error on deleting document {e}') + return {} + + @staticmethod + def validate_pdf(file, content_length, document_type) -> Optional[list]: + """Validate the PDF file.""" + msg = [] + verify_paper_size = document_type in [ + DocumentTypeEnum.CNTO, + DocumentTypeEnum.DIRECTOR_AFFIDAVIT + ] + + try: + pdf_reader = PyPDF2.PdfFileReader(file) + if verify_paper_size: + # Check that all pages in the pdf are letter size and able to be processed. + if any(x.mediaBox.getWidth() != 612 or x.mediaBox.getHeight() != 792 for x in pdf_reader.pages): + msg.append({'error': _('Document must be set to fit onto 8.5” x 11” letter-size paper.'), + 'path': file.filename}) + if content_length > 30000000: + msg.append({'error': _('File exceeds maximum size.'), 'path': file.filename}) + + if pdf_reader.isEncrypted: + msg.append({'error': _('File must be unencrypted.'), 'path': file.filename}) + + except Exception as e: + msg.append({'error': _('Invalid file.'), 'path': file.filename}) + current_app.logger.debug(e) + + if msg: + return msg + + return None diff --git a/legal-api/src/legal_api/services/filings/validations/common_validations.py b/legal-api/src/legal_api/services/filings/validations/common_validations.py index a624e296a6..f58ad3f2e8 100644 --- a/legal-api/src/legal_api/services/filings/validations/common_validations.py +++ b/legal-api/src/legal_api/services/filings/validations/common_validations.py @@ -22,7 +22,7 @@ from legal_api.errors import Error from legal_api.models import Business -from legal_api.services import MinioService, flags, namex +from legal_api.services import MinioService, flags, namex, DocumentRecordService from legal_api.services.utils import get_str from legal_api.utils.datetime import datetime as dt @@ -329,3 +329,12 @@ def validate_foreign_jurisdiction(foreign_jurisdiction: dict, msg.append({'error': 'Invalid region.', 'path': f'{foreign_jurisdiction_path}/region'}) return msg + +def validate_file_on_drs(document_class: str, document_service_id: str, path) -> bool: + """Validate file existence on DRS""" + msg = [] + doc = DocumentRecordService.get_document(document_class, document_service_id) + if not bool(doc.get("documentURL")): + msg.append({'error': 'File does not exist on Document Record Service', 'path': path}) + + return msg \ No newline at end of file diff --git a/legal-api/src/legal_api/services/filings/validations/continuation_in.py b/legal-api/src/legal_api/services/filings/validations/continuation_in.py index 08ec8e0286..40b43f01cd 100644 --- a/legal-api/src/legal_api/services/filings/validations/continuation_in.py +++ b/legal-api/src/legal_api/services/filings/validations/continuation_in.py @@ -25,8 +25,8 @@ validate_foreign_jurisdiction, validate_name_request, validate_parties_names, - validate_pdf, validate_share_structure, + validate_file_on_drs ) from legal_api.services.filings.validations.incorporation_application import ( validate_incorporation_effective_date, @@ -35,6 +35,7 @@ ) from legal_api.services.utils import get_bool, get_str from legal_api.utils.datetime import datetime as dt +from legal_api.constants import DocumentClassEnum def validate(filing_json: dict) -> Optional[Error]: # pylint: disable=too-many-branches; @@ -127,7 +128,7 @@ def _validate_foreign_jurisdiction(filing_json: dict, filing_type: str, legal_ty ((region := foreign_jurisdiction.get('region')) and region == 'AB')): affidavit_file_key_path = f'{foreign_jurisdiction_path}/affidavitFileKey' if file_key := foreign_jurisdiction.get('affidavitFileKey'): - if err := validate_pdf(file_key, affidavit_file_key_path, False): + if err := validate_file_on_drs(DocumentClassEnum.CORP, file_key, affidavit_file_key_path): msg.extend(err) else: msg.append({'error': 'Affidavit from the directors is required.', 'path': affidavit_file_key_path}) @@ -157,7 +158,7 @@ def validate_continuation_in_authorization(filing_json: dict, filing_type: str) for index, file in enumerate(filing_json['filing'][filing_type]['authorization']['files']): file_key = file['fileKey'] file_key_path = f'{authorization_path}/files/{index}/fileKey' - if err := validate_pdf(file_key, file_key_path, False): + if err := validate_file_on_drs(DocumentClassEnum.CORP, file_key, file_key_path): msg.extend(err) return msg diff --git a/queue_services/entity-filer/src/entity_filer/filing_processors/continuation_in.py b/queue_services/entity-filer/src/entity_filer/filing_processors/continuation_in.py index 393c30db69..c803536d1a 100644 --- a/queue_services/entity-filer/src/entity_filer/filing_processors/continuation_in.py +++ b/queue_services/entity-filer/src/entity_filer/filing_processors/continuation_in.py @@ -17,6 +17,7 @@ from entity_queue_common.service_utils import QueueException from legal_api.models import Business, Document, DocumentType, Filing, Jurisdiction +from legal_api.services import DocumentRecordService from legal_api.utils.legislation_datetime import LegislationDatetime from entity_filer.filing_meta import FilingMeta @@ -160,4 +161,12 @@ def process(business: Business, # pylint: disable=too-many-branches,too-many-lo filing_json['filing']['business']['legalType'] = business.legal_type filing_json['filing']['business']['foundingDate'] = business.founding_date.isoformat() filing_rec._filing_json = filing_json # pylint: disable=protected-access; bypass to update filing data + # Get a file key from continuation in object. + files = continuation_in.get('authorization', {}).get('files', []) + if not len(files): + raise QueueException( + f'continuationIn {filing_rec.id}, Unable to update business identifier on Document Record Service.' + ) + # Update business identifier on Document Record Service + DocumentRecordService.update_business_identifier(business.identifier, files[0].get('fileKey')) return business, filing_rec, filing_meta