Skip to content
Open
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
5 changes: 5 additions & 0 deletions legal-api/src/legal_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions legal-api/src/legal_api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions legal-api/src/legal_api/resources/v2/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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('/<string:document_class>/<string:document_type>', 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/<string:document_service_id>', 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/<string:document_class>/<string:document_service_id>', 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
1 change: 1 addition & 0 deletions legal-api/src/legal_api/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
177 changes: 177 additions & 0 deletions legal-api/src/legal_api/services/document_record.py
Original file line number Diff line number Diff line change
@@ -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 },
Copy link

Copilot AI May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The key 'consumerIdentifer' is misspelled; it should be 'consumerIdentifier'. Additionally, the exception logging message below incorrectly references deletion instead of an update.

Suggested change
url, json={ 'consumerIdentifer': business_identifier },
url, json={ 'consumerIdentifier': business_identifier },

Copilot uses AI. Check for mistakes.
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
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Copy link

Copilot AI May 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return type annotation '-> bool' does not match the implementation which returns a list of errors. Consider updating the annotation to Optional[list] or a similar appropriate type.

Suggested change
def validate_file_on_drs(document_class: str, document_service_id: str, path) -> bool:
def validate_file_on_drs(document_class: str, document_service_id: str, path) -> list:

Copilot uses AI. Check for mistakes.
"""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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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})
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading