Skip to content

Commit f6546a7

Browse files
authored
23351 - Notice of Withdrawal Emailer (bcgov#3195)
* add NoW email template * update tracker * add NoW email processor * update unit tests
1 parent 5b48624 commit f6546a7

9 files changed

Lines changed: 426 additions & 3 deletions

File tree

queue_services/entity-emailer/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ webcolors==1.13
7878
Werkzeug==1.0.1
7979
yarl==1.8.2
8080
zipp==3.15.0
81-
git+https://github.com/bcgov/business-schemas.git@2.18.27#egg=registry_schemas
81+
git+https://github.com/bcgov/business-schemas.git@2.18.33#egg=registry_schemas
8282
git+https://github.com/bcgov/lear.git#egg=legal_api&subdirectory=legal-api
8383
git+https://github.com/bcgov/lear.git#egg=entity_queue_common&subdirectory=queue_services/common
8484
git+https://github.com/bcgov/lear.git#egg=sql-versioning&subdirectory=python/common/sql-versioning
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Copyright © 2025 Province of British Columbia
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Email processing rules and actions for Notice of Withdrawal notifications."""
15+
import base64
16+
import re
17+
from http import HTTPStatus
18+
from pathlib import Path
19+
20+
import requests
21+
from entity_queue_common.service_utils import logger
22+
from flask import current_app
23+
from jinja2 import Template
24+
from legal_api.core.meta.filing import FilingMeta
25+
from legal_api.models import Business, Filing
26+
27+
from entity_emailer.email_processors import (
28+
get_filing_document,
29+
get_filing_info,
30+
get_recipient_from_auth,
31+
substitute_template_parts,
32+
)
33+
34+
35+
def process(email_info: dict, token: str) -> dict: # pylint: disable=too-many-locals
36+
"""Build the email for Notice of Withdrawal notification."""
37+
logger.debug('notice_of_withdrawal_notification: %s', email_info)
38+
# get template and fill in parts
39+
filing_type = email_info['type']
40+
41+
# get template variables from filing
42+
filing, business, leg_tmz_filing_date, leg_tmz_effective_date = get_filing_info(email_info['filingId'])
43+
44+
# display company name only for existing businesses
45+
if business.get('identifier').startswith('T'):
46+
company_name = None
47+
else:
48+
company_name = business.get('legalName')
49+
# record to be withdrawn --> withdrawn filing display name
50+
withdrawn_filing = Filing.find_by_id(filing.withdrawn_filing_id)
51+
withdrawn_filing_display_name = FilingMeta.get_display_name(
52+
business['legalType'],
53+
withdrawn_filing.filing_type,
54+
withdrawn_filing.filing_sub_type
55+
)
56+
template = Path(
57+
f'{current_app.config.get("TEMPLATE_PATH")}/NOW-COMPLETED.html'
58+
).read_text()
59+
filled_template = substitute_template_parts(template)
60+
# render template with vars
61+
jnja_template = Template(filled_template, autoescape=True)
62+
filing_data = (filing.json)['filing'][f'{filing_type}']
63+
filing_name = filing.filing_type[0].upper() + ' '.join(re.findall('[a-zA-Z][^A-Z]*', filing.filing_type[1:]))
64+
html_out = jnja_template.render(
65+
business=business,
66+
filing=filing_data,
67+
header=(filing.json)['filing']['header'],
68+
company_name=company_name,
69+
filing_date_time=leg_tmz_filing_date,
70+
effective_date_time=leg_tmz_effective_date,
71+
withdrawnFilingType=withdrawn_filing_display_name,
72+
entity_dashboard_url=current_app.config.get('DASHBOARD_URL') +
73+
(filing.json)['filing']['business'].get('identifier', ''),
74+
email_header=filing_name.upper(),
75+
filing_type=filing_type
76+
)
77+
78+
# get attachments
79+
pdfs = _get_pdfs(token, business, filing, leg_tmz_filing_date, leg_tmz_effective_date)
80+
81+
# get recipients
82+
identifier = filing.filing_json['filing']['business']['identifier']
83+
recipients = _get_contacts(identifier, token, withdrawn_filing)
84+
recipients = list(set(recipients))
85+
recipients = ', '.join(filter(None, recipients)).strip()
86+
87+
# assign subject
88+
subject = 'Notice of Withdrawal filed Successfully'
89+
90+
legal_name = business.get('legalName', None)
91+
legal_name = 'Numbered Company' if legal_name.startswith(identifier) else legal_name
92+
if not identifier.startswith('T'):
93+
subject = f'{legal_name} - {subject}' if legal_name else subject
94+
95+
return {
96+
'recipients': recipients,
97+
'requestBy': 'BCRegistries@gov.bc.ca',
98+
'content': {
99+
'subject': subject,
100+
'body': f'{html_out}',
101+
'attachments': pdfs
102+
}
103+
}
104+
105+
106+
def _get_pdfs(
107+
token: str,
108+
business: dict,
109+
filing: Filing,
110+
filing_date_time: str,
111+
effective_date: str) -> list:
112+
"""Get the PDFs for the Notice of Withdrawal output."""
113+
pdfs = []
114+
attach_order = 1
115+
headers = {
116+
'Accept': 'application/pdf',
117+
'Authorization': f'Bearer {token}'
118+
}
119+
120+
# add filing PDF
121+
filing_pdf_type = 'noticeOfWithdrawal'
122+
filing_pdf_encoded = get_filing_document(business['identifier'], filing.id, filing_pdf_type, token)
123+
if filing_pdf_encoded:
124+
pdfs.append(
125+
{
126+
'fileName': 'Notice of Withdrawal.pdf',
127+
'fileBytes': filing_pdf_encoded.decode('utf-8'),
128+
'fileUrl': '',
129+
'attachOrder': str(attach_order)
130+
}
131+
)
132+
attach_order += 1
133+
134+
# add receipt PDF
135+
corp_name = business.get('legalName')
136+
if business.get('identifier').startswith('T'):
137+
business_data = None
138+
else:
139+
business_data = Business.find_by_internal_id(filing.business_id)
140+
receipt = requests.post(
141+
f'{current_app.config.get("PAY_API_URL")}/{filing.payment_token}/receipts',
142+
json={
143+
'corpName': corp_name,
144+
'filingDateTime': filing_date_time,
145+
'effectiveDateTime': effective_date if effective_date else '',
146+
'filingIdentifier': str(filing.id),
147+
'businessNumber': business_data.tax_id if business_data and business_data.tax_id else ''
148+
}, headers=headers)
149+
150+
if receipt.status_code != HTTPStatus.CREATED:
151+
logger.error('Failed to get receipt pdf for filing: %s', filing.id)
152+
else:
153+
receipt_encoded = base64.b64encode(receipt.content)
154+
pdfs.append(
155+
{
156+
'fileName': 'Receipt.pdf',
157+
'fileBytes': receipt_encoded.decode('utf-8'),
158+
'fileUrl': '',
159+
'attachOrder': str(attach_order)
160+
})
161+
attach_order += 1
162+
return pdfs
163+
164+
165+
def _get_contacts(identifier, token, withdrawn_filing):
166+
recipients = []
167+
if identifier.startswith('T'):
168+
# get from withdrawn filing (FE new business filing)
169+
filing_type = withdrawn_filing.filing_type
170+
recipients.append(withdrawn_filing.filing_json['filing'][filing_type]['contactPoint']['email'])
171+
172+
for party in withdrawn_filing.filing_json['filing'][filing_type]['parties']:
173+
for role in party['roles']:
174+
if role['roleType'] == 'Completing Party':
175+
recipients.append(party['officer'].get('email'))
176+
break
177+
else:
178+
recipients.append(get_recipient_from_auth(identifier, token))
179+
180+
return recipients
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<base href="/">
6+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
8+
<meta name="referrer" content="origin-when-cross-origin" />
9+
<meta name="author" content="BC Registries and Online Services">
10+
<title>Notice of Withdrawal</title>
11+
[[style.html]]
12+
</head>
13+
14+
<body>
15+
<table class="body-table" role="presentation">
16+
<tr>
17+
<td>
18+
[[header.html]]
19+
20+
<div class="container">
21+
<p class="title-message bold">
22+
Your record has been successfully withdrawn
23+
</p>
24+
25+
[[20px.html]]
26+
[[divider.html]]
27+
[[20px.html]]
28+
{% if company_name %}
29+
<p class="now-filing-info-title">Company Name:</p>
30+
<p>{{ company_name }}</p>
31+
[[16px.html]]
32+
{% endif %}
33+
<p class="now-filing-info-title">Date and Time of Filing:</p>
34+
<p>{{ filing_date_time}}</p>
35+
36+
[[16px.html]]
37+
<p class="now-filing-info-title">Effective Date and Time:</p>
38+
<p>{{ effective_date_time}}</p>
39+
40+
[[16px.html]]
41+
<p class="now-filing-info-title">Record to be Withdrawn:</p>
42+
<p>{{ withdrawnFilingType }}</p>
43+
[[20px.html]]
44+
[[divider.html]]
45+
[[20px.html]]
46+
47+
<p>The following documents are attached to this email:</p>
48+
<ul class="outputs">
49+
<li>Notice of Withdrawal</li>
50+
<li>Receipt</li>
51+
</ul>
52+
53+
[[business-dashboard-link.html]]
54+
55+
[[20px.html]]
56+
[[footer.html]]
57+
</div>
58+
</td>
59+
</tr>
60+
</table>
61+
</body>
62+
63+
</html>

queue_services/entity-emailer/src/entity_emailer/email_templates/common/style.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,9 @@
8989
.continuation-application-details .value {
9090
line-height: 24px;
9191
}
92+
93+
.now-filing-info-title {
94+
font-weight: 700;
95+
margin-bottom: 4px;
96+
}
9297
</style>

queue_services/entity-emailer/src/entity_emailer/message_tracker/tracker.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def get_message_context_properties(queue_msg: nats.aio.client.Msg):
9797
message_id = f'{etype}_{option}_{ar_year}_{business_id}'
9898
return create_message_context_properties(etype, message_id, None, None, False)
9999

100-
if etype in ('agmLocationChange', 'agmExtension') \
100+
if etype in ('agmLocationChange', 'agmExtension', 'noticeOfWithdrawal') \
101101
and (option := email.get('option', None)) \
102102
and option == 'COMPLETED' \
103103
and (filing_id := email.get('filingId', None)):

queue_services/entity-emailer/src/entity_emailer/worker.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
involuntary_dissolution_stage_1_notification,
5959
mras_notification,
6060
name_request,
61+
notice_of_withdrawal_notification,
6162
nr_notification,
6263
registration_notification,
6364
restoration_notification,
@@ -215,6 +216,9 @@ def process_email(email_msg: dict, flask_app: Flask): # pylint: disable=too-man
215216
elif etype == 'continuationIn':
216217
email = continuation_in_notification.process(email_msg['email'], token)
217218
send_email(email, token)
219+
elif etype == 'noticeOfWithdrawal' and option == Filing.Status.COMPLETED.value:
220+
email = notice_of_withdrawal_notification.process(email_msg['email'], token)
221+
send_email(email, token)
218222
elif etype in filing_notification.FILING_TYPE_CONVERTER.keys():
219223
if etype == 'annualReport' and option == Filing.Status.COMPLETED.value:
220224
logger.debug('No email to send for: %s', email_msg)

queue_services/entity-emailer/tests/unit/__init__.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"""The Unit Tests and the helper routines."""
1515
import copy
1616
import json
17-
from datetime import datetime
17+
from datetime import datetime, timedelta
1818
from random import randrange
1919
from unittest.mock import Mock
2020

@@ -41,6 +41,7 @@
4141
FILING_HEADER,
4242
FILING_TEMPLATE,
4343
INCORPORATION_FILING_TEMPLATE,
44+
NOTICE_OF_WITHDRAWAL,
4445
REGISTRATION,
4546
RESTORATION,
4647
)
@@ -698,6 +699,86 @@ def prep_continuation_in_filing(session, identifier, payment_id, option):
698699
return filing
699700

700701

702+
def prep_notice_of_withdraw_filing(
703+
identifier,
704+
payment_id,
705+
legal_type,
706+
legal_name,
707+
business_id,
708+
withdrawn_filing):
709+
"""Return a new Notice of Withdrawal filing prepped for email notification."""
710+
filing_template = copy.deepcopy(FILING_HEADER)
711+
filing_template['filing']['header']['name'] = 'noticeOfWithdrawal'
712+
713+
filing_template['filing']['noticeOfWithdrawal'] = copy.deepcopy(NOTICE_OF_WITHDRAWAL)
714+
filing_template['filing']['noticeOfWithdrawal']['filingId'] = withdrawn_filing.id
715+
filing_template['filing']['business'] = {
716+
'identifier': identifier,
717+
'legalType': legal_type,
718+
'legalName': legal_name
719+
}
720+
721+
# create NoW filing
722+
filing = create_filing(
723+
token=payment_id,
724+
filing_json=filing_template,
725+
business_id=business_id,
726+
)
727+
# populate NoW related properties
728+
filing.withdrawn_filing_id = withdrawn_filing.id
729+
filing.save()
730+
withdrawn_filing.withdrawal_pending = True
731+
withdrawn_filing.save()
732+
733+
return filing
734+
735+
736+
def create_future_effective_filing(
737+
identifier,
738+
legal_type,
739+
legal_name,
740+
filing_type,
741+
filing_json,
742+
is_temp,
743+
business_id=None):
744+
"""Create a future effective filing."""
745+
filing_template = copy.deepcopy(FILING_HEADER)
746+
filing_template['filing']['header']['name'] = filing_type
747+
future_effective_date = EPOCH_DATETIME + timedelta(days=5)
748+
future_effective_date = future_effective_date.isoformat()
749+
750+
if is_temp:
751+
del filing_template['filing']['business']
752+
new_business_filing_json = copy.deepcopy(filing_json)
753+
new_business_filing_json['nameRequest']['legalType'] = legal_type
754+
filing_template['filing'][filing_type] = new_business_filing_json
755+
filing_template['filing'][filing_type]['contactPoint']['email'] = 'recipient@email.com'
756+
else:
757+
filing_template['filing']['business']['identifier'] = identifier
758+
filing_template['filing']['business'] = {
759+
'identifier': identifier,
760+
'legalType': legal_type,
761+
'legalName': legal_name
762+
}
763+
fe_filing_json = copy.deepcopy(filing_json)
764+
filing_template['filing'][filing_type] = fe_filing_json
765+
766+
fe_filing = Filing()
767+
fe_filing.filing_date = EPOCH_DATETIME
768+
fe_filing.filing_json = filing_template
769+
fe_filing.save()
770+
fe_filing.payment_token = '123'
771+
fe_filing.payment_completion_date = EPOCH_DATETIME.isoformat()
772+
if is_temp:
773+
fe_filing.temp_reg = identifier
774+
else:
775+
fe_filing.business_id = business_id
776+
fe_filing.effective_date = future_effective_date
777+
fe_filing.save()
778+
779+
return fe_filing
780+
781+
701782
class Obj:
702783
"""Make a custom object hook used by dict_to_obj."""
703784

0 commit comments

Comments
 (0)