Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

22235 Legal API - Create endpoint for retrieving stage 1 AR letter #2849

Merged
merged 14 commits into from
Jul 24, 2024
Merged
23 changes: 8 additions & 15 deletions jobs/furnishings/src/furnishings/stage_processors/stage_one.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,31 +84,24 @@ 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,
business.last_ar_date if business.last_ar_date else business.founding_date,
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+
Expand Down
81 changes: 81 additions & 0 deletions legal-api/report-templates/noticeOfDissolutionCommencement.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Notice of Commencement of Dissolution</title>
<meta charset="UTF-8">
<meta name="author" content="BC Registries and Online Services">
{% if outputType == 'email' %}
[[common/v2/style.html]]
{% else %}
[[common/v2/styleMail.html]]
{% endif %}
</head>
<body>
<div class="letter-copy">
{% if furnishing.mailingAddress %}
<div class="container address-container ml-3">
{% if furnishing.mailingAddress.streetAddressAdditional %}
<div class="address-container-additional-info">
{% else %}
<div class="address-container-no-additional-info">
{% endif %}
<div>{{ furnishing.businessName }}</div>
<div>{{ furnishing.mailingAddress.streetAddress }}</div>
<div>{{ furnishing.mailingAddress.streetAddressAdditional }}</div>
<div>
{{ furnishing.mailingAddress.addressCity }}
{{ furnishing.mailingAddress.addressRegion }}
&nbsp;{{ furnishing.mailingAddress.postalCode }}
</div>
</div>
</div>
{% endif %}
<div class="container letter-container ml-3">
<div class="mt-5"><span class="bold">No Annual Reports Filed Since {{ furnishing.lastARDate }} for {{ furnishing.businessIdentifier }}</span></div>
<div class="mt-5">To file an annual report online, log in to your Business Page at <a href="https://www.business.bcregistry.gov.bc.ca/{{ furnishing.businessIdentifier }}" class="break-url">business.bcregistry.gov.bc.ca/<wbr>{{ furnishing.businessIdentifier }}</a> to file any outstanding annual reports listed.</div>
<div class="mt-5">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.</div>
<div class="mt-5">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.</div>

{% if furnishing.foreignRegistrations %}
<div class="mt-5">
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.
</div>
{% endif %}

<div class="mt-5">To request a delay of the dissolution, go to <a href="https://www.business.bcregistry.gov.bc.ca/{{ furnishing.businessIdentifier }}" class="break-url">business.bcregistry.gov.bc.ca/{{ furnishing.businessIdentifier }}</a> and request for a Delay of Dissolution or Cancellation under the To Do section. This must be completed prior to the dissolution of the company.</div>
<div class="mt-5">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.</div>
<div class="mt-5">If you have filed the outstanding annual reports, no further action is required.</div>
<div class="mt-5">If you need help with setting up an account or managing a business, please visit our Resources and Help page at <a href="https://bcreg.ca/resources" class="break-url">bcreg.ca/resources</a></div>

<p class="mt-5"><i><span class="bold">Issued</span> on my behalf on {{ furnishing.processedDate }}</i></p>
<div class="registrar-info">
<div>[[common/certificateRegistrarSignature.html]]</div>
<div>
<div class="registrar-name"><span class="bold">{{ registrarInfo.name }}</span></div>
<div class="registrar-title">{{ registrarInfo.title }}</div>
</div>
<div class="mt-2">
<div class="registry-info"><span class="bold">BC Registries and Online Services</span></div>
<div class="registry-contact"><span class="bold">Toll-Free Phone:</span> 1-877-526-1526</div>
</div>
</div>
</div>
</div>
</body>
</html>
68 changes: 68 additions & 0 deletions legal-api/report-templates/template-parts/common/v2/footer.html

Large diffs are not rendered by default.

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions legal-api/report-templates/template-parts/common/v2/header.html

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions legal-api/report-templates/template-parts/common/v2/headerMail.html

Large diffs are not rendered by default.

603 changes: 603 additions & 0 deletions legal-api/report-templates/template-parts/common/v2/style.html

Large diffs are not rendered by default.

635 changes: 635 additions & 0 deletions legal-api/report-templates/template-parts/common/v2/styleMail.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions legal-api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions legal-api/src/legal_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
GOTENBERG_REPORT_API_AUDIENCE = os.getenv('GOTENBERG_REPORT_API_AUDIENCE', '')
GOTENBERG_REPORT_SVC_URL = os.getenv('GOTENBERG_REPORT_SVC_URL', 'http://')
argush3 marked this conversation as resolved.
Show resolved Hide resolved

TESTING = False
DEBUG = False

Expand Down
1 change: 1 addition & 0 deletions legal-api/src/legal_api/exceptions/error_messages/codes.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ class ErrorCode(AutoName):
FILING_NOT_FOUND = auto()
MISSING_BUSINESS = auto()
NOT_AUTHORIZED = auto()
FURNISHING_NOT_FOUND = auto()
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
}
242 changes: 242 additions & 0 deletions legal-api/src/legal_api/reports/report_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
# 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, output_type=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._output_type = output_type

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('GOTENBERG_REPORT_SVC_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._output_type:
self._report_data['outputType'] = self._output_type
else:
self._report_data['outputType'] = 'email'
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
elif (mailing_address := self._business.mailing_address.one_or_none()):
self._report_data['furnishing']['mailingAddress'] = mailing_address.json
argush3 marked this conversation as resolved.
Show resolved Hide resolved

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._output_type == 'email':
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('GOTENBERG_REPORT_API_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'
}
}
Loading
Loading