diff --git a/jobs/furnishings/src/furnishings/stage_processors/stage_one.py b/jobs/furnishings/src/furnishings/stage_processors/stage_one.py
index d20e2021fd..9070296e1c 100644
--- a/jobs/furnishings/src/furnishings/stage_processors/stage_one.py
+++ b/jobs/furnishings/src/furnishings/stage_processors/stage_one.py
@@ -84,9 +84,7 @@ async def _send_first_round_notification(self, batch_processing: BatchProcessing
# send email/letter notification for the first time
email = self._get_email_address_from_auth(batch_processing.business_identifier)
business = Business.find_by_identifier(batch_processing.business_identifier)
- if email:
- # send email letter
- new_furnishing = self._create_new_furnishing(
+ new_furnishing = self._create_new_furnishing(
batch_processing,
eligible_details,
Furnishing.FurnishingType.EMAIL,
@@ -94,21 +92,16 @@ async def _send_first_round_notification(self, batch_processing: BatchProcessing
business.legal_name,
email
)
- # notify emailer
+ mailing_address = business.mailing_address.one_or_none()
+ if mailing_address:
+ self._create_furnishing_address(mailing_address, new_furnishing.id)
+ if email:
+ # send email letter
await self._send_email(new_furnishing)
else:
# send paper letter if business doesn't have email address
- new_furnishing = self._create_new_furnishing(
- batch_processing,
- eligible_details,
- Furnishing.FurnishingType.MAIL,
- business.last_ar_date if business.last_ar_date else business.founding_date,
- business.legal_name
- )
-
- mailing_address = business.mailing_address.one_or_none()
- if mailing_address:
- self._create_furnishing_address(mailing_address, new_furnishing.id)
+ new_furnishing.furnishing_type = Furnishing.FurnishingType.MAIL
+ new_furnishing.save()
# TODO: create and add letter to either AR or transition pdf
# TODO: send AR and transition pdf to BCMail+
@@ -192,7 +185,7 @@ def _create_new_furnishing( # pylint: disable=too-many-arguments
def _create_furnishing_address(self, mailing_address: Address, furnishings_id: int) -> Address:
"""Clone business mailing address to be used by mail furnishings."""
furnishing_address = Address(
- address_type=Address.FURNISHING,
+ address_type=mailing_address.address_type,
street=mailing_address.street,
street_additional=mailing_address.street_additional,
city=mailing_address.city,
diff --git a/jobs/furnishings/tests/unit/stage_processors/test_stage_one.py b/jobs/furnishings/tests/unit/stage_processors/test_stage_one.py
index a5fe966481..9a9a8ed479 100644
--- a/jobs/furnishings/tests/unit/stage_processors/test_stage_one.py
+++ b/jobs/furnishings/tests/unit/stage_processors/test_stage_one.py
@@ -88,7 +88,7 @@ def test_get_email_address_from_auth(session, test_name, mock_return):
async def test_process_first_notification(app, session, test_name, entity_type, email, expected_furnishing_name):
"""Assert that the first notification furnishing entry is created correctly."""
business = factory_business(identifier='BC1234567', entity_type=entity_type)
- factory_address(address_type=Address.MAILING, business_id=business.id)
+ mailing_address = factory_address(address_type=Address.MAILING, business_id=business.id)
batch = factory_batch()
factory_batch_processing(
batch_id=batch.id,
@@ -129,7 +129,7 @@ async def test_process_first_notification(app, session, test_name, entity_type,
assert len(furnishing_addresses) == 1
furnishing_address = furnishing_addresses[0]
assert furnishing_address
- assert furnishing_address.address_type == Address.FURNISHING
+ assert furnishing_address.address_type == mailing_address.address_type
assert furnishing_address.furnishings_id == furnishing.id
assert furnishing_address.business_id == None
assert furnishing_address.office_id == None
@@ -160,7 +160,7 @@ async def test_process_first_notification(app, session, test_name, entity_type,
async def test_process_second_notification(app, session, test_name, has_email_furnishing, has_mail_furnishing, is_email_elapsed):
"""Assert that the second notification furnishing entry is created correctly."""
business = factory_business(identifier='BC1234567')
- factory_address(address_type=Address.MAILING, business_id=business.id)
+ mailing_address = factory_address(address_type=Address.MAILING, business_id=business.id)
batch = factory_batch()
factory_batch_processing(
batch_id=batch.id,
@@ -210,7 +210,7 @@ async def test_process_second_notification(app, session, test_name, has_email_fu
assert len(furnishing_addresses) == 1
furnishing_address = furnishing_addresses[0]
assert furnishing_address
- assert furnishing_address.address_type == Address.FURNISHING
+ assert furnishing_address.address_type == mailing_address.address_type
assert furnishing_address.furnishings_id == mail_furnishing.id
assert furnishing_address.business_id == None
assert furnishing_address.office_id == None
diff --git a/legal-api/devops/vaults.json b/legal-api/devops/vaults.json
index 27ef04a298..cdc7e9a3c8 100644
--- a/legal-api/devops/vaults.json
+++ b/legal-api/devops/vaults.json
@@ -39,7 +39,8 @@
{
"vault": "api",
"application": [
- "mras-api"
+ "mras-api",
+ "report-api-gotenberg"
]
}
]
diff --git a/legal-api/report-templates/noticeOfDissolutionCommencement.html b/legal-api/report-templates/noticeOfDissolutionCommencement.html
new file mode 100644
index 0000000000..e1dbc0aa49
--- /dev/null
+++ b/legal-api/report-templates/noticeOfDissolutionCommencement.html
@@ -0,0 +1,81 @@
+
+
+
+ Notice of Commencement of Dissolution
+
+
+ {% if variant == 'default' %}
+ [[common/v2/style.html]]
+ {% else %}
+ [[common/v2/styleMail.html]]
+ {% endif %}
+
+
+
+ {% if furnishing.mailingAddress %}
+
+ {% if furnishing.mailingAddress.streetAddressAdditional %}
+
+ {% else %}
+
+ {% endif %}
+
{{ furnishing.businessName }}
+
{{ furnishing.mailingAddress.streetAddress }}
+
{{ furnishing.mailingAddress.streetAddressAdditional }}
+
+ {{ furnishing.mailingAddress.addressCity }}
+ {{ furnishing.mailingAddress.addressRegion }}
+ {{ furnishing.mailingAddress.postalCode }}
+
+
+
+ {% endif %}
+
+
No Annual Reports Filed Since {{ furnishing.lastARDate }} for {{ furnishing.businessIdentifier }}
+
+
Under section 422 of the Business Corporation Act (the Act), this letter is to notify you that your company has for two years failed to file the annual reports required under section 51 of the Act. A company must annually, within two months after each anniversary of the date on which the company was recognized, file an annual report with the Registrar.
+
If within one month after the date of this letter, the company fails to file the outstanding annual reports, a notice may be published on the BC Laws website www.bclaws.ca. This notice will state that, at any time after the expiration of one month after the date of publication of the notice, the company will be dissolved, unless cause is shown to the contrary; I am satisfied the failure has been or is being remedied; or a copy of the entered court order to the contrary has been filed.
+
+ {% if furnishing.foreignRegistrations %}
+
+ Our records indicate your company is registered in
+ {% if furnishing.foreignRegistrations|length == 1 %}
+ {{ furnishing.foreignRegistrations[0] }}
+ {% elif furnishing.foreignRegistrations|length == 2 %}
+ {{ furnishing.foreignRegistrations[0] }} and {{ furnishing.foreignRegistrations[1] }}
+ {% else %}
+ {{ furnishing.foreignRegistrations[0] }}, {{ furnishing.foreignRegistrations[1] }}, and {{ furnishing.foreignRegistrations[2] }}
+ {% endif %}
+ as an extraprovincial company. Therefore, if your company is dissolved, its registration as an extraprovincial company in
+ {% if furnishing.foreignRegistrations|length == 1 %}
+ {{ furnishing.foreignRegistrations[0] }}
+ {% elif furnishing.foreignRegistrations|length == 2 %}
+ {{ furnishing.foreignRegistrations[0] }} and {{ furnishing.foreignRegistrations[1] }}
+ {% else %}
+ {{ furnishing.foreignRegistrations[0] }}, {{ furnishing.foreignRegistrations[1] }}, and {{ furnishing.foreignRegistrations[2] }}
+ {% endif %}
+ will automatically be cancelled as well.
+
+ {% endif %}
+
+
+
If your company is dissolved under section 422(1)(a) of the Act, section 347 of the Act states the liability of each director, officer, shareholder and liquidator of a company that is dissolved continues and may be enforced as if the company had not been dissolved.
+
If you have filed the outstanding annual reports, no further action is required.
+
If you need help with setting up an account or managing a business, please visit our Resources and Help page at
bcreg.ca/resources
+
+
Issued on my behalf on {{ furnishing.processedDate }}
+
+
[[common/certificateRegistrarSignature.html]]
+
+
{{ registrarInfo.name }}
+
{{ registrarInfo.title }}
+
+
+
BC Registries and Online Services
+
Toll-Free Phone: 1-877-526-1526
+
+
+
+
+
+
diff --git a/legal-api/report-templates/template-parts/common/v2/footer.html b/legal-api/report-templates/template-parts/common/v2/footer.html
new file mode 100644
index 0000000000..327baf5792
--- /dev/null
+++ b/legal-api/report-templates/template-parts/common/v2/footer.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
diff --git a/legal-api/report-templates/template-parts/common/v2/footerMail.html b/legal-api/report-templates/template-parts/common/v2/footerMail.html
new file mode 100644
index 0000000000..10bb2c1869
--- /dev/null
+++ b/legal-api/report-templates/template-parts/common/v2/footerMail.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
diff --git a/legal-api/report-templates/template-parts/common/v2/header.html b/legal-api/report-templates/template-parts/common/v2/header.html
new file mode 100644
index 0000000000..52e8b1a6ca
--- /dev/null
+++ b/legal-api/report-templates/template-parts/common/v2/header.html
@@ -0,0 +1,108 @@
+
+
+
+
Reg Report
+
+
+
+
+
+
diff --git a/legal-api/report-templates/template-parts/common/v2/headerMail.html b/legal-api/report-templates/template-parts/common/v2/headerMail.html
new file mode 100644
index 0000000000..b7a740aaee
--- /dev/null
+++ b/legal-api/report-templates/template-parts/common/v2/headerMail.html
@@ -0,0 +1,108 @@
+
+
+
+
Reg Report
+
+
+
+
+
+
diff --git a/legal-api/report-templates/template-parts/common/v2/style.html b/legal-api/report-templates/template-parts/common/v2/style.html
new file mode 100644
index 0000000000..c8a13f8a97
--- /dev/null
+++ b/legal-api/report-templates/template-parts/common/v2/style.html
@@ -0,0 +1,603 @@
+
diff --git a/legal-api/report-templates/template-parts/common/v2/styleMail.html b/legal-api/report-templates/template-parts/common/v2/styleMail.html
new file mode 100644
index 0000000000..260a93b039
--- /dev/null
+++ b/legal-api/report-templates/template-parts/common/v2/styleMail.html
@@ -0,0 +1,635 @@
+
diff --git a/legal-api/requirements.txt b/legal-api/requirements.txt
index 6cbd539327..9b9dbd94cc 100755
--- a/legal-api/requirements.txt
+++ b/legal-api/requirements.txt
@@ -30,6 +30,7 @@ ecdsa==0.14.1
expiringdict==1.1.4
flask-jwt-oidc==0.3.0
flask-restx==0.3.0
+google-auth==2.16.2
gunicorn==20.1.0
idna==2.10
itsdangerous==1.1.0
diff --git a/legal-api/src/legal_api/config.py b/legal-api/src/legal_api/config.py
index efb2b61988..b673a9a560 100644
--- a/legal-api/src/legal_api/config.py
+++ b/legal-api/src/legal_api/config.py
@@ -171,6 +171,10 @@ class _Config(): # pylint: disable=too-few-public-methods
MRAS_SVC_URL = os.getenv('MRAS_SVC_URL')
MRAS_SVC_API_KEY = os.getenv('MRAS_SVC_API_KEY')
+ # GCP Gotenberg report service
+ REPORT_API_GOTENBERG_AUDIENCE = os.getenv('REPORT_API_GOTENBERG_AUDIENCE', '')
+ REPORT_API_GOTENBERG_URL = os.getenv('REPORT_API_GOTENBERG_URL', 'https://')
+
TESTING = False
DEBUG = False
diff --git a/legal-api/src/legal_api/exceptions/error_messages/codes.py b/legal-api/src/legal_api/exceptions/error_messages/codes.py
index f62e1bccab..8ba8476c1e 100644
--- a/legal-api/src/legal_api/exceptions/error_messages/codes.py
+++ b/legal-api/src/legal_api/exceptions/error_messages/codes.py
@@ -32,3 +32,4 @@ class ErrorCode(AutoName):
FILING_NOT_FOUND = auto()
MISSING_BUSINESS = auto()
NOT_AUTHORIZED = auto()
+ FURNISHING_NOT_FOUND = auto()
diff --git a/legal-api/src/legal_api/exceptions/error_messages/messages.py b/legal-api/src/legal_api/exceptions/error_messages/messages.py
index 826dc79b09..f8e7c77d40 100644
--- a/legal-api/src/legal_api/exceptions/error_messages/messages.py
+++ b/legal-api/src/legal_api/exceptions/error_messages/messages.py
@@ -19,4 +19,5 @@
ErrorCode.MISSING_BUSINESS: 'Business not found for identifier: {identifier}',
ErrorCode.FILING_NOT_FOUND: 'Filing: {filing_id} not found for: {identifier}',
ErrorCode.NOT_AUTHORIZED: 'Not authorized to access business: {identifier}',
+ ErrorCode.FURNISHING_NOT_FOUND: 'Furnishing: {furnishing_id} not found for identifier: {identifier}'
}
diff --git a/legal-api/src/legal_api/models/address.py b/legal-api/src/legal_api/models/address.py
index b8ce59188d..91c78bb126 100644
--- a/legal-api/src/legal_api/models/address.py
+++ b/legal-api/src/legal_api/models/address.py
@@ -32,8 +32,7 @@ class Address(db.Model): # pylint: disable=too-many-instance-attributes
MAILING = 'mailing'
DELIVERY = 'delivery'
- FURNISHING = 'furnishing'
- ADDRESS_TYPES = [MAILING, DELIVERY, FURNISHING]
+ ADDRESS_TYPES = [MAILING, DELIVERY]
JSON_MAILING = 'mailingAddress'
JSON_DELIVERY = 'deliveryAddress'
JSON_ADDRESS_TYPES = [JSON_MAILING, JSON_DELIVERY]
diff --git a/legal-api/src/legal_api/reports/report_v2.py b/legal-api/src/legal_api/reports/report_v2.py
new file mode 100644
index 0000000000..7dfe946c0b
--- /dev/null
+++ b/legal-api/src/legal_api/reports/report_v2.py
@@ -0,0 +1,240 @@
+# Copyright © 2024 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.
+"""Produces a PDF output for Furnishing based on templates and JSON messages."""
+import copy
+from enum import auto
+from http import HTTPStatus
+from pathlib import Path
+from typing import Final
+
+import google.auth.transport.requests
+import google.oauth2.id_token
+import requests
+from flask import current_app, jsonify
+from jinja2 import Template
+
+from legal_api.models import Address
+from legal_api.reports.registrar_meta import RegistrarInfo
+from legal_api.services import MrasService
+from legal_api.utils.base import BaseEnum
+from legal_api.utils.legislation_datetime import LegislationDatetime
+
+
+OUTPUT_DATE_FORMAT: Final = '%B %-d, %Y'
+SINGLE_URI: Final = '/forms/chromium/convert/html'
+HEADER_PATH: Final = '/template-parts/common/v2/header.html'
+HEADER_MAIL_PATH: Final = '/template-parts/common/v2/headerMail.html'
+FOOTER_PATH: Final = '/template-parts/common/v2/footer.html'
+FOOTER_MAIL_PATH: Final = '/template-parts/common/v2/footerMail.html'
+HEADER_TITLE_REPLACE: Final = '{{TITLE}}'
+REPORT_META_DATA = {
+ 'marginTop': 1.93,
+ 'marginLeft': 0.4,
+ 'marginRight': 0.4,
+ 'marginBottom': 0.9,
+ 'printBackground': True
+}
+REPORT_FILES = {
+ 'index.html': '',
+ 'header.html': '',
+ 'footer.html': ''
+}
+
+
+class ReportV2:
+ """Service to create Gotenberg document outputs."""
+
+ def __init__(self, business, furnishing, document_key, variant=None):
+ """Create ReportV2 instance."""
+ self._furnishing = furnishing
+ self._business = business
+ self._document_key = document_key
+ self._report_data = None
+ self._report_date_time = LegislationDatetime.now()
+ self._variant = variant
+
+ def get_pdf(self):
+ """Render the furnishing document pdf response."""
+ headers = {}
+ token = ReportV2.get_report_api_token()
+ if token:
+ headers['Authorization'] = 'Bearer {}'.format(token)
+ url = current_app.config.get('REPORT_API_GOTENBERG_URL') + SINGLE_URI
+ data = {
+ 'reportName': self._get_report_filename(),
+ 'template': self._get_template(),
+ 'templateVars': self._get_template_data()
+ }
+ files = self._get_report_files(data)
+ response = requests.post(url=url, headers=headers, data=REPORT_META_DATA, files=files, timeout=1800.0)
+
+ if response.status_code != HTTPStatus.OK:
+ return jsonify(message=str(response.content)), response.status_code
+
+ # return response.content, response.status_code
+ return current_app.response_class(
+ response=response.content,
+ status=response.status_code,
+ mimetype='application/pdf'
+ )
+
+ def _get_report_filename(self):
+ report_date = str(self._report_date_time)[:19]
+ return '{}_{}_{}.pdf'.format(self._business.identifier, report_date,
+ ReportMeta.reports[self._document_key]['reportName']).replace(' ', '_')
+
+ def _get_template(self):
+ try:
+ template_path = current_app.config.get('REPORT_TEMPLATE_PATH')
+ template_file_name = ReportMeta.reports[self._document_key]['templateName']
+ template_code = Path(f'{template_path}/{template_file_name}.html').read_text(encoding='UTF-8')
+ # substitute template parts
+ template_code = self._substitute_template_parts(template_code)
+ except Exception as err:
+ current_app.logger.error(err)
+ raise err
+ return template_code
+
+ @staticmethod
+ def _substitute_template_parts(template_code):
+ template_path = current_app.config.get('REPORT_TEMPLATE_PATH')
+ template_parts = [
+ 'common/v2/style',
+ 'common/v2/styleMail',
+ 'common/certificateRegistrarSignature'
+ ]
+ # substitute template parts - marked up by [[filename]]
+ for template_part in template_parts:
+ template_part_code = Path(f'{template_path}/template-parts/{template_part}.html')\
+ .read_text(encoding='UTF-8')
+ template_code = template_code.replace('[[{}.html]]'.format(template_part), template_part_code)
+ return template_code
+
+ def _get_template_data(self):
+ self._report_data = {}
+ self._format_furnishing_data()
+ self._set_meta_info()
+ self._set_address()
+ self._set_registrar_info()
+ if self._document_key == ReportTypes.DISSOLUTION:
+ self._set_ep_registration()
+ return self._report_data
+
+ def _format_furnishing_data(self):
+ self._report_data['furnishing'] = {
+ 'businessName': self._furnishing.business_name,
+ 'businessIdentifier': self._furnishing.business_identifier
+ }
+
+ if self._furnishing.last_ar_date:
+ last_ar_date = LegislationDatetime.as_legislation_timezone(self._furnishing.last_ar_date)
+ else:
+ last_ar_date = LegislationDatetime.as_legislation_timezone(self._business.founding_date)
+ self._report_data['furnishing']['lastARDate'] = last_ar_date.strftime(OUTPUT_DATE_FORMAT)
+
+ if self._furnishing.processed_date:
+ processed_date = LegislationDatetime.as_legislation_timezone(self._furnishing.processed_date)
+ else:
+ processed_date = LegislationDatetime.as_legislation_timezone(self._report_date_time)
+ self._report_data['furnishing']['processedDate'] = processed_date.strftime(OUTPUT_DATE_FORMAT)
+
+ def _set_meta_info(self):
+ if self._variant:
+ self._report_data['variant'] = self._variant
+ else:
+ self._report_data['variant'] = 'default'
+ self._report_data['title'] = ReportMeta.reports[self._document_key]['reportDescription'].upper()
+
+ def _set_address(self):
+ if (furnishing_address := Address.find_by(furnishings_id=self._furnishing.id)):
+ furnishing_address = furnishing_address[0]
+ self._report_data['furnishing']['mailingAddress'] = furnishing_address.json
+
+ def _set_registrar_info(self):
+ if self._furnishing.processed_date:
+ self._report_data['registrarInfo'] = {**RegistrarInfo.get_registrar_info(self._furnishing.processed_date)}
+ else:
+ self._report_data['registrarInfo'] = {**RegistrarInfo.get_registrar_info(self._report_date_time)}
+
+ def _set_ep_registration(self):
+ jurisdictions = MrasService.get_jurisdictions(self._furnishing.business_identifier)
+ if jurisdictions:
+ ep_registrations = [e['name'] for e in jurisdictions if e['id'] in ['AB', 'SK', 'MB']]
+ ep_registrations.sort()
+ self._report_data['furnishing']['foreignRegistrations'] = ep_registrations
+ else:
+ self._report_data['furnishing']['foreignRegistrations'] = []
+
+ def _get_report_files(self, data):
+ """Get gotenberg report generation source file data."""
+ title = self._report_data['title']
+ files = copy.deepcopy(REPORT_FILES)
+ files['index.html'] = self._get_html_from_data(data)
+ if self._variant == 'default':
+ files['header.html'] = self._get_html_from_path(HEADER_PATH, title)
+ files['footer.html'] = self._get_html_from_path(FOOTER_PATH)
+ else:
+ files['header.html'] = self._get_html_from_path(HEADER_MAIL_PATH, title)
+ files['footer.html'] = self._get_html_from_path(FOOTER_MAIL_PATH)
+
+ return files
+
+ @staticmethod
+ def _get_html_from_data(data):
+ """Get html by merging the template with the report data."""
+ html_output = None
+ try:
+ template = Template(data['template'], autoescape=True)
+ html_output = template.render(data['templateVars'])
+ except Exception as err:
+ current_app.logger.error('Error rendering HTML template: ' + str(err))
+ return html_output
+
+ @staticmethod
+ def _get_html_from_path(path, title=None):
+ html_template = None
+ try:
+ template_path = current_app.config.get('REPORT_TEMPLATE_PATH') + path
+ html_template = Path(template_path).read_text(encoding='UTF-8')
+ if title:
+ html_template = html_template.replace(HEADER_TITLE_REPLACE, title)
+ except Exception as err:
+ current_app.logger.error(f'Error loading HTML template from path={template_path}: ' + str(err))
+ return html_template
+
+ @staticmethod
+ def get_report_api_token():
+ """Generate access token for Gotenberg Report API."""
+ audience = current_app.config.get('REPORT_API_GOTENBERG_AUDIENCE')
+ if not audience:
+ return None
+ auth_req = google.auth.transport.requests.Request()
+ token = google.oauth2.id_token.fetch_id_token(auth_req, audience)
+ current_app.logger.info('Obtained token for Gotenberg Report API.')
+ return token
+
+
+class ReportTypes(BaseEnum):
+ """Render an Enum of the Gotenberg report types."""
+
+ DISSOLUTION = auto()
+
+
+class ReportMeta:
+ """Helper class to maintain the report meta information."""
+
+ reports = {
+ ReportTypes.DISSOLUTION: {
+ 'reportName': 'dissoluion',
+ 'templateName': 'noticeOfDissolutionCommencement',
+ 'reportDescription': 'Notice of Commencement of Dissolution'
+ }
+ }
diff --git a/legal-api/src/legal_api/resources/v2/business/__init__.py b/legal-api/src/legal_api/resources/v2/business/__init__.py
index a25986f34d..07d64d3c89 100644
--- a/legal-api/src/legal_api/resources/v2/business/__init__.py
+++ b/legal-api/src/legal_api/resources/v2/business/__init__.py
@@ -23,6 +23,7 @@
from .business_directors import get_directors
from .business_documents import get_business_documents
from .business_filings import delete_filings, get_documents, get_filings, patch_filings, saving_filings
+from .business_furnishings import get_furnishing_document
from .business_parties import get_parties
from .business_resolutions import get_resolutions
from .business_share_classes import get_share_class
diff --git a/legal-api/src/legal_api/resources/v2/business/business_furnishings/__init__.py b/legal-api/src/legal_api/resources/v2/business/business_furnishings/__init__.py
new file mode 100644
index 0000000000..8cf38f256f
--- /dev/null
+++ b/legal-api/src/legal_api/resources/v2/business/business_furnishings/__init__.py
@@ -0,0 +1,21 @@
+# Copyright © 2024 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.
+"""Business Furnishings.
+
+Provides all furnishing entries externalized services.
+"""
+from .furnishing_documents import get_furnishing_document
+
+
+__all__ = ('get_furnishing_document',)
diff --git a/legal-api/src/legal_api/resources/v2/business/business_furnishings/furnishing_documents.py b/legal-api/src/legal_api/resources/v2/business/business_furnishings/furnishing_documents.py
new file mode 100644
index 0000000000..17d636dcf4
--- /dev/null
+++ b/legal-api/src/legal_api/resources/v2/business/business_furnishings/furnishing_documents.py
@@ -0,0 +1,65 @@
+# Copyright © 2024 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.
+"""Retrieve the specified letter for the furnishing entry."""
+from http import HTTPStatus
+from typing import Final
+
+from flask import jsonify, request
+from flask_cors import cross_origin
+
+from legal_api.exceptions import ErrorCode, get_error_message
+from legal_api.models import Business, Furnishing, UserRoles
+from legal_api.reports.report_v2 import ReportTypes, ReportV2
+from legal_api.services import authorized
+from legal_api.utils.auth import jwt
+
+from ..bp import bp
+
+
+FURNISHING_DOC_BASE_ROUTE: Final = '/
/furnishings//document'
+
+
+@bp.route(FURNISHING_DOC_BASE_ROUTE, methods=['GET', 'OPTIONS'])
+@cross_origin(origins='*')
+@jwt.has_one_of_roles([UserRoles.system, UserRoles.staff])
+def get_furnishing_document(identifier: str, furnishing_id: int):
+ """Return a JSON object with meta information about the Service."""
+ # basic checks
+ if not authorized(identifier, jwt, ['view', ]):
+ return jsonify(
+ message=get_error_message(ErrorCode.NOT_AUTHORIZED, **{'identifier': identifier})
+ ), HTTPStatus.UNAUTHORIZED
+
+ if not (business := Business.find_by_identifier(identifier)):
+ return jsonify(
+ message=get_error_message(ErrorCode.MISSING_BUSINESS,
+ **{'identifier': identifier})
+ ), HTTPStatus.NOT_FOUND
+ if not (furnishing := Furnishing.find_by_id(furnishing_id)) or furnishing.business_id != business.id:
+ return jsonify(
+ message=get_error_message(ErrorCode.FURNISHING_NOT_FOUND,
+ **{'furnishing_id': furnishing_id, 'identifier': identifier})
+ ), HTTPStatus.NOT_FOUND
+
+ variant = request.args.get('variant', 'default').lower()
+ if variant not in ['default', 'greyscale']:
+ return jsonify({'message': f'{variant} not a valid variant'}), HTTPStatus.BAD_REQUEST
+
+ if 'application/pdf' in request.accept_mimetypes:
+ try:
+ return ReportV2(business, furnishing, ReportTypes.DISSOLUTION, variant).get_pdf()
+ except Exception:
+ return jsonify({'message': 'Unable to get furnishing document.'}), HTTPStatus.INTERNAL_SERVER_ERROR
+
+ return {}, HTTPStatus.NOT_FOUND
diff --git a/legal-api/tests/unit/models/__init__.py b/legal-api/tests/unit/models/__init__.py
index 5105bc616c..e722148d74 100644
--- a/legal-api/tests/unit/models/__init__.py
+++ b/legal-api/tests/unit/models/__init__.py
@@ -32,6 +32,7 @@
Business,
Comment,
Filing,
+ Furnishing,
Office,
Party,
PartyRole,
@@ -421,3 +422,47 @@ def factory_batch_processing(batch_id,
)
batch_processing.save()
return batch_processing
+
+
+def factory_furnishing(business_id,
+ business_identifier,
+ batch_id,
+ furnishing_type=Furnishing.FurnishingType.EMAIL,
+ furnishing_name=Furnishing.FurnishingName.DISSOLUTION_COMMENCEMENT_NO_AR,
+ status=Furnishing.FurnishingStatus.QUEUED,
+ created_date=datetime.utcnow(),
+ last_modified=datetime.utcnow(),
+ processed_date=datetime.utcnow(),
+ last_ar_date=None,
+ business_name=None
+ ):
+ """Create a furnishing."""
+ furnishing = Furnishing(
+ business_id=business_id,
+ business_identifier=business_identifier,
+ batch_id=batch_id,
+ furnishing_type=furnishing_type,
+ furnishing_name=furnishing_name,
+ status=status,
+ created_date=created_date,
+ last_modified=last_modified,
+ processed_date=processed_date,
+ last_ar_date=last_ar_date,
+ business_name=business_name
+ )
+
+ furnishing.save()
+ return furnishing
+
+
+def factory_business_with_stage_one_furnishing():
+ """Create a business with a stage one furnishing entry."""
+ business = factory_business('BC1234567')
+ factory_business_mailing_address(business)
+ batch = factory_batch()
+ furnishing = factory_furnishing(business_id=business.id,
+ business_identifier=business.identifier,
+ batch_id=batch.id,
+ last_ar_date=EPOCH_DATETIME,
+ business_name='TEST-BUSINESS')
+ return business, furnishing
diff --git a/legal-api/tests/unit/reports/test_report_v2.py b/legal-api/tests/unit/reports/test_report_v2.py
new file mode 100644
index 0000000000..8209ca9919
--- /dev/null
+++ b/legal-api/tests/unit/reports/test_report_v2.py
@@ -0,0 +1,48 @@
+# Copyright © 2024 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.
+"""Test-Suite to ensure that the ReportV2(Gotenberg Report) class is working as expected."""
+import pytest
+
+from unittest.mock import patch
+from legal_api.services import MrasService
+from legal_api.reports.report_v2 import ReportV2, ReportTypes
+from tests.unit.models import factory_business_with_stage_one_furnishing
+
+
+@pytest.mark.parametrize(
+ 'test_name, variant', [
+ ('COMMENCEMENT_DEFAULT', 'default'),
+ ('COMMENCEMENT_GREYSCALE', 'greyscale'),
+ ]
+)
+def test_get_pdf(session, test_name, variant):
+ """Assert that furnishing can be returned as a Gotenberg PDF."""
+ business, furnishing = factory_business_with_stage_one_furnishing()
+ with patch.object(MrasService, 'get_jurisdictions', return_value=[]):
+ report = ReportV2(business, furnishing, ReportTypes.DISSOLUTION, variant)
+ filename = report._get_report_filename()
+ assert filename
+ template = report._get_template()
+ assert template
+ template_data = report._get_template_data()
+ assert template_data
+ assert template_data['furnishing']
+ assert template_data['variant'] == variant
+ assert template_data['registrarInfo']
+ assert template_data['title'] == 'NOTICE OF COMMENCEMENT OF DISSOLUTION'
+ report_files = report._get_report_files(template_data)
+ assert report_files
+ assert 'header.html' in report_files
+ assert 'index.html' in report_files
+ assert 'footer.html' in report_files
diff --git a/legal-api/tests/unit/resources/v2/test_business_furnishings/__init__.py b/legal-api/tests/unit/resources/v2/test_business_furnishings/__init__.py
new file mode 100644
index 0000000000..30b067ad02
--- /dev/null
+++ b/legal-api/tests/unit/resources/v2/test_business_furnishings/__init__.py
@@ -0,0 +1,14 @@
+# Copyright © 2024 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.
+"""Test-Suite for the businesses/{}/furnishings API."""
\ No newline at end of file
diff --git a/legal-api/tests/unit/resources/v2/test_business_furnishings/test_furnishing_documents.py b/legal-api/tests/unit/resources/v2/test_business_furnishings/test_furnishing_documents.py
new file mode 100644
index 0000000000..c59a2d2181
--- /dev/null
+++ b/legal-api/tests/unit/resources/v2/test_business_furnishings/test_furnishing_documents.py
@@ -0,0 +1,101 @@
+# Copyright © 2024 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.
+
+"""Tests to assure the business-furnishings end-point
+
+Test-Suite to ensure that the /businesses/_id_/furnishings endpoint is working as expected.
+"""
+import pytest
+from http import HTTPStatus
+from unittest.mock import patch
+
+from legal_api.models import UserRoles
+from legal_api.reports.report_v2 import ReportV2
+from tests.unit.models import factory_business_with_stage_one_furnishing
+from tests.unit.services.utils import create_header
+
+
+
+def test_get_furnishing_document(session, client, jwt):
+ """Assert that the endpoint is worked as expected."""
+
+ business, furnishing = factory_business_with_stage_one_furnishing()
+ with patch.object(ReportV2, 'get_pdf', return_value=('', HTTPStatus.OK)):
+ rv = client.get(f'/api/v2/businesses/{business.identifier}/furnishings/{furnishing.id}/document',
+ headers=create_header(jwt, [UserRoles.system, ], business.identifier, **{'accept': 'application/pdf'}))
+
+ assert rv
+ assert rv.status_code == HTTPStatus.OK
+
+
+def test_get_furnishing_document_invalid_role(session, client, jwt):
+ """Assert the call fails for invalid user role."""
+ business, furnishing = factory_business_with_stage_one_furnishing()
+ rv = client.get(f'/api/v2/businesses/{business.identifier}/furnishings/{furnishing.id}/document',
+ headers=create_header(jwt, [UserRoles.basic, ], business.identifier, **{'accept': 'application/pdf'}))
+
+ assert rv
+ assert rv.status_code == HTTPStatus.UNAUTHORIZED
+ code = rv.json.get('code')
+ assert code == 'missing_a_valid_role'
+
+
+def test_get_furnishing_document_missing_business(session, client, jwt):
+ business, furnishing = factory_business_with_stage_one_furnishing()
+ invalid_identifier = 'ABC'
+ rv = client.get(f'/api/v2/businesses/{invalid_identifier}/furnishings/{furnishing.id}/document',
+ headers=create_header(jwt, [UserRoles.system, ], business.identifier, **{'accept': 'application/pdf'}))
+
+ assert rv
+ assert rv.status_code == HTTPStatus.NOT_FOUND
+ message = rv.json.get('message')
+ assert message
+ assert invalid_identifier in message
+
+
+def test_get_furnishing_document_missing_furnishing(session, client, jwt):
+ business, furnishing = factory_business_with_stage_one_furnishing()
+ invalid_furnishing_id = '123456789'
+ rv = client.get(f'/api/v2/businesses/{business.identifier}/furnishings/{invalid_furnishing_id}/document',
+ headers=create_header(jwt, [UserRoles.system, ], business.identifier, **{'accept': 'application/pdf'}))
+
+ assert rv
+ assert rv.status_code == HTTPStatus.NOT_FOUND
+ message = rv.json.get('message')
+ assert message
+ assert business.identifier in message
+ assert invalid_furnishing_id in message
+
+@pytest.mark.parametrize(
+ 'test_name, variant, valid', [
+ ('TEST_DEFAULT', 'default', True),
+ ('TEST_GREYSCALE', 'greyscale', True),
+ ('TEST_VALID_CASE_INSENSITIVE', 'dEFAULT', True),
+ ('TEST_INVALID', 'paper', False)
+ ]
+)
+def test_get_furnishing_document_variant(session, client, jwt, test_name, variant, valid):
+ business, furnishing = factory_business_with_stage_one_furnishing()
+ with patch.object(ReportV2, 'get_pdf', return_value=('', HTTPStatus.OK)):
+ rv = client.get(f'/api/v2/businesses/{business.identifier}/furnishings/{furnishing.id}/document?variant={variant}',
+ headers=create_header(jwt, [UserRoles.system, ], business.identifier, **{'accept': 'application/pdf'}))
+
+ assert rv
+ if valid:
+ assert rv.status_code == HTTPStatus.OK
+ else:
+ assert rv.status_code == HTTPStatus.BAD_REQUEST
+ message = rv.json.get('message')
+ assert message
+ assert variant in message