Skip to content

Commit 3ec6e07

Browse files
Use Document Service (bcgov#3592)
1 parent f93b2f3 commit 3ec6e07

File tree

10 files changed

+345
-3
lines changed

10 files changed

+345
-3
lines changed

legal-api/.env.sample

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,7 @@ BUSINESS_CRED_DEF_ID=
8989

9090
# Digital Wallet Attestation credential
9191
WALLET_CRED_DEF_ID=
92+
93+
DOCUMENT_API_URL=
94+
DOCUMENT_API_VERSION=/api/v1
95+
DOCUMENT_API_KEY=

legal-api/devops/vaults.gcp.env

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,8 @@ NATS_CLIENT_NAME="entity.filing.filer.worker"
9797
NATS_FILER_SUBJECT="entity.filing.filer"
9898
NATS_ENTITY_EVENT_SUBJECT="entity.events"
9999
NATS_EMAILER_SUBJECT="entity.email"
100+
101+
#doc service
102+
DOCUMENT_API_URL="op://API/$APP_ENV/doc-api/DOC_API_URL"
103+
DOCUMENT_API_VERSION="op://API/$APP_ENV/doc-api/DOC_API_VERSION"
104+
DOCUMENT_API_KEY="op://API/$APP_ENV/doc-api/DOC_API_KEY"

legal-api/src/legal_api/config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,15 @@ class _Config: # pylint: disable=too-few-public-methods
226226
NATS_EMAILER_SUBJECT = os.getenv('NATS_EMAILER_SUBJECT', 'entity.email')
227227
NATS_QUEUE = os.getenv('NATS_QUEUE', 'entity-filer-worker')
228228

229+
# Document Service
230+
DOCUMENT_API_URL = os.getenv('DOCUMENT_API_URL')
231+
DOCUMENT_API_VERSION = os.getenv('DOCUMENT_API_VERSION')
232+
DOCUMENT_SVC_URL = ''
233+
if DOCUMENT_API_URL and DOCUMENT_API_VERSION:
234+
DOCUMENT_SVC_URL = f'{DOCUMENT_API_URL + DOCUMENT_API_VERSION}/documents'
235+
DOCUMENT_PRODUCT_CODE = 'BUSINESS'
236+
DOCUMENT_API_KEY = os.getenv('DOCUMENT_API_KEY')
237+
229238
TESTING = False
230239
DEBUG = False
231240

@@ -343,6 +352,10 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods
343352
TRACTION_PUBLIC_SCHEMA_DID = os.getenv('TRACTION_PUBLIC_SCHEMA_DID', 'TRACTION_PUBLIC_SCHEMA_DID')
344353
TRACTION_PUBLIC_ISSUER_DID = os.getenv('TRACTION_PUBLIC_ISSUER_DID', 'TRACTION_PUBLIC_ISSUER_DID')
345354

355+
DOCUMENT_API_URL = 'http://document-api.com'
356+
DOCUMENT_API_VERSION = os.getenv('DOCUMENT_API_VERSION', '/api/v1')
357+
DOCUMENT_SVC_URL = f'{DOCUMENT_API_URL + DOCUMENT_API_VERSION}/documents'
358+
346359

347360
class ProdConfig(_Config): # pylint: disable=too-few-public-methods
348361
"""Production environment configuration."""

legal-api/src/legal_api/models/document.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,12 @@ def find_by_business_id_and_type(cls, business_id: int, document_type: str):
7979
def find_by_file_key(cls, file_key: str):
8080
"""Return the document matching the file key."""
8181
return cls.query.filter_by(file_key=file_key).one_or_none()
82+
83+
@classmethod
84+
def find_one_by(cls, business_id: int, filing_id: int, document_type: str):
85+
"""Return the document matching the business id, filing id and document type."""
86+
return cls.query.filter_by(
87+
business_id=business_id,
88+
filing_id=filing_id,
89+
type=document_type
90+
).order_by(desc(Document.id)).first()

legal-api/src/legal_api/reports/business_document.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from flask import current_app, jsonify
2323

2424
from legal_api.models import Alias, AmalgamatingBusiness, Amalgamation, Business, CorpType, Filing, Jurisdiction
25+
from legal_api.reports.document_service import DocumentService
2526
from legal_api.reports.registrar_meta import RegistrarInfo
2627
from legal_api.resources.v2.business import get_addresses, get_directors
2728
from legal_api.resources.v2.business.business_parties import get_parties
@@ -43,6 +44,7 @@ def __init__(self, business, document_key):
4344
self._report_date_time = LegislationDatetime.now()
4445
self._epoch_filing_date = None
4546
self._tombstone_filing_date = None
47+
self._document_service = DocumentService()
4648

4749
def get_pdf(self):
4850
"""Render the business document pdf response."""
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Copyright © 2025 Province of British Columbia
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
4+
# the License. You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
9+
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
10+
# specific language governing permissions and limitations under the License.
11+
"""Works with the document service api."""
12+
13+
import json
14+
from http import HTTPStatus
15+
16+
import requests
17+
from flask import current_app, jsonify
18+
19+
from legal_api.exceptions import BusinessException
20+
from legal_api.models import Business, Document
21+
from legal_api.services import AccountService
22+
23+
24+
class DocumentService:
25+
"""Service to create document records in document service api."""
26+
27+
def __init__(self):
28+
"""Initialize the document service."""
29+
self.url = current_app.config.get('DOCUMENT_SVC_URL')
30+
self.product_code = current_app.config.get('DOCUMENT_PRODUCT_CODE')
31+
self.api_key = current_app.config.get('DOCUMENT_API_KEY')
32+
33+
def get_content(self, response):
34+
"""Get the content of the response useful for test methods."""
35+
c = response.content
36+
try:
37+
c = c.decode()
38+
c = json.loads(c)
39+
except Exception:
40+
pass
41+
return c
42+
43+
# pylint: disable=too-many-arguments
44+
def create_document_record(
45+
self,
46+
business_id: int,
47+
filing_id: int,
48+
report_type: str,
49+
file_key: str,
50+
file_name: str):
51+
"""Create a document record in the document table."""
52+
new_document = Document(
53+
business_id=business_id,
54+
filing_id=filing_id,
55+
type=report_type,
56+
file_key=file_key,
57+
file_name=file_name,
58+
)
59+
new_document.save()
60+
61+
def has_document(
62+
self,
63+
business_identifier: str,
64+
filing_identifier: int,
65+
report_type: str):
66+
"""
67+
Check if a document exists in the document service.
68+
69+
business_identifier: The business identifier.
70+
filing_identifier: The filing identifier.
71+
report_type: The report type.
72+
account_id: The account id.
73+
return: True if the document exists, False otherwise.
74+
"""
75+
business_id = Business.find_by_identifier(business_identifier).id
76+
document = Document.find_one_by(
77+
business_id,
78+
filing_identifier,
79+
report_type)
80+
return document if document else False
81+
82+
# pylint: disable=too-many-arguments
83+
def create_document(
84+
self,
85+
business_identifier: str,
86+
filing_identifier: int,
87+
report_type: str,
88+
account_id: str,
89+
binary_or_url):
90+
"""
91+
Create a document in the document service.
92+
93+
business_identifier: The business identifier.
94+
filing_identifier: The filing identifier.
95+
report_type: The report type.
96+
account_id: The account id.
97+
binary_or_url: The binary (pdf) or url of the document.
98+
"""
99+
if self.has_document(business_identifier, filing_identifier, report_type):
100+
raise BusinessException('Document already exists', HTTPStatus.CONFLICT)
101+
102+
token = AccountService.get_bearer_token()
103+
headers = {
104+
'Content-Type': 'application/json',
105+
'X-ApiKey': self.api_key,
106+
'Account-Id': account_id,
107+
'Authorization': 'Bearer ' + token
108+
}
109+
post_url = (f'{self.url}/application-reports/'
110+
f'{self.product_code}/{business_identifier}/'
111+
f'{filing_identifier}/{report_type}')
112+
response = requests.post(url=post_url, headers=headers, data=binary_or_url)
113+
content = self.get_content(response)
114+
if response.status_code != HTTPStatus.CREATED:
115+
return jsonify(message=str(content)), response.status_code
116+
self.create_document_record(
117+
Business.find_by_identifier(business_identifier).id,
118+
filing_identifier, report_type, content['identifier'],
119+
f'{business_identifier}_{filing_identifier}_{report_type}.pdf'
120+
)
121+
return content, response.status_code
122+
123+
# pylint: disable=too-many-arguments
124+
def get_document(
125+
self,
126+
business_identifier: str,
127+
filing_identifier: int,
128+
report_type: str,
129+
account_id: str,
130+
file_key: str = None):
131+
"""
132+
Get a document from the document service.
133+
134+
business_identifier: The business identifier.
135+
filing_identifier: The filing identifier.
136+
report_type: The report type.
137+
account_id: The account id.
138+
return: The document url (or binary).
139+
"""
140+
token = AccountService.get_bearer_token()
141+
headers = {
142+
'X-ApiKey': self.api_key,
143+
'Account-Id': account_id,
144+
'Content-Type': 'application/pdf',
145+
'Authorization': 'Bearer ' + token
146+
}
147+
get_url = ''
148+
if file_key is not None:
149+
get_url = f'{self.url}/application-reports/{self.product_code}/{file_key}'
150+
else:
151+
document = self.has_document(business_identifier, filing_identifier, report_type)
152+
if document is False:
153+
raise BusinessException('Document not found', HTTPStatus.NOT_FOUND)
154+
get_url = f'{self.url}/application-reports/{self.product_code}/{document.file_key}'
155+
156+
if get_url != '':
157+
response = requests.get(url=get_url, headers=headers)
158+
content = self.get_content(response)
159+
if response.status_code != HTTPStatus.OK:
160+
return jsonify(message=str(content)), response.status_code
161+
return content, response.status_code
162+
return jsonify(message='Document not found'), HTTPStatus.NOT_FOUND

legal-api/src/legal_api/reports/report.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import pycountry
2323
import requests
2424
from dateutil.relativedelta import relativedelta
25-
from flask import current_app, jsonify
25+
from flask import current_app, jsonify, request
2626

2727
from legal_api.core.meta.filing import FILINGS, FilingMeta
2828
from legal_api.models import (
@@ -37,8 +37,9 @@
3737
PartyRole,
3838
)
3939
from legal_api.models.business import ASSOCIATION_TYPE_DESC
40+
from legal_api.reports.document_service import DocumentService
4041
from legal_api.reports.registrar_meta import RegistrarInfo
41-
from legal_api.services import MinioService, VersionedBusinessDetailsService, flags
42+
from legal_api.services import MinioService, VersionedBusinessDetailsService, flags # pylint: disable=line-too-long
4243
from legal_api.utils.auth import jwt
4344
from legal_api.utils.datetime import timezone
4445
from legal_api.utils.formatting import float_to_str
@@ -58,6 +59,7 @@ def __init__(self, filing):
5859
self._business = None
5960
self._report_key = None
6061
self._report_date_time = LegislationDatetime.now()
62+
self._document_service = DocumentService()
6163

6264
def get_pdf(self, report_type=None):
6365
"""Render a pdf for the report."""
@@ -77,6 +79,21 @@ def _get_static_report(self):
7779
)
7880

7981
def _get_report(self):
82+
account_id = request.headers.get('Account-Id', None)
83+
if account_id is not None and self._business is not None:
84+
document, status = self._document_service.get_document(
85+
self._business.identifier,
86+
self._filing.id,
87+
self._report_key,
88+
account_id
89+
)
90+
if status == HTTPStatus.OK:
91+
return current_app.response_class(
92+
response=document,
93+
status=status,
94+
mimetype='application/pdf'
95+
)
96+
8097
if self._filing.business_id:
8198
self._business = Business.find_by_internal_id(self._filing.business_id)
8299
Report._populate_business_info_to_filing(self._filing, self._business)
@@ -97,6 +114,28 @@ def _get_report(self):
97114
if response.status_code != HTTPStatus.OK:
98115
return jsonify(message=str(response.content)), response.status_code
99116

117+
create_document = account_id is not None
118+
create_filing_types = [
119+
'incorporationApplication',
120+
'continuationIn',
121+
'amalgamation',
122+
'registration'
123+
]
124+
if self._filing.filing_type in create_filing_types:
125+
create_document = create_document and self._business and self._business.tax_id
126+
else:
127+
create_document = create_document and \
128+
self._filing.status == 'COMPLETED'
129+
130+
if create_document:
131+
self._document_service.create_document(
132+
self._business.identifier,
133+
self._filing.identifier,
134+
self._report_key,
135+
account_id,
136+
response.content
137+
)
138+
100139
return current_app.response_class(
101140
response=response.content,
102141
status=response.status_code,

legal-api/tests/conftest.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,17 @@
1515
import datetime
1616
import time
1717
from contextlib import contextmanager, suppress
18-
18+
import re
1919
import pytest
20+
import json
21+
from http import HTTPStatus
22+
2023
from flask_migrate import Migrate, upgrade
2124
from sqlalchemy import event, text
2225
from sqlalchemy.schema import DropConstraint, MetaData
26+
from unittest import mock
27+
import requests_mock
28+
import os
2329

2430
from legal_api import create_app
2531
from legal_api import jwt as _jwt
@@ -207,3 +213,26 @@ def minio_server(docker_services):
207213
with suppress(Exception):
208214
docker_services.wait_for_service('minio', 9000)
209215
time.sleep(10)
216+
217+
218+
DOCUMENT_API_URL = 'http://document-api.com'
219+
DOCUMENT_API_VERSION = '/api/v1'
220+
DOCUMENT_SVC_URL = f'{DOCUMENT_API_URL + DOCUMENT_API_VERSION}/documents'
221+
DOCUMENT_PRODUCT_CODE = 'BUSINESS'
222+
223+
@pytest.fixture()
224+
def mock_doc_service():
225+
mock_response = {
226+
'identifier': 1,
227+
'url': 'https://document-service.com/document/1'
228+
}
229+
with requests_mock.Mocker(real_http=True) as mock:
230+
post_url = f'{DOCUMENT_SVC_URL}/application-reports/{DOCUMENT_PRODUCT_CODE}/'
231+
mock.post(re.compile(f"{post_url}.*"),
232+
status_code=HTTPStatus.CREATED,
233+
text=json.dumps(mock_response))
234+
get_url = f'{DOCUMENT_SVC_URL}/application-reports/{DOCUMENT_PRODUCT_CODE}/'
235+
mock.get(re.compile(f"{get_url}.*"),
236+
status_code=HTTPStatus.OK,
237+
text=json.dumps(mock_response))
238+
yield mock

0 commit comments

Comments
 (0)