-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
18053 Legal API - Validation (AGM Extension) (#2298)
* 18053 Signed-off-by: Hongjing Chen <[email protected]> * add new line Signed-off-by: Hongjing Chen <[email protected]> * update agm validation Signed-off-by: Hongjing Chen <[email protected]> * update error msg Signed-off-by: Hongjing Chen <[email protected]> * update unit tests Signed-off-by: Hongjing Chen <[email protected]> * correct typos Signed-off-by: Hongjing Chen <[email protected]> * more validations Signed-off-by: Hongjing Chen <[email protected]> * fix linting Signed-off-by: Hongjing Chen <[email protected]> * fix linting-2 Signed-off-by: Hongjing Chen <[email protected]> * add more validation for required fields Signed-off-by: Hongjing Chen <[email protected]> * fix linting Signed-off-by: Hongjing Chen <[email protected]> * convert some datetime to date Signed-off-by: Hongjing Chen <[email protected]> * update Signed-off-by: Hongjing Chen <[email protected]> * fix bugs Signed-off-by: Hongjing Chen <[email protected]> * remove validation for intended agm date Signed-off-by: Hongjing Chen <[email protected]> * fix partial code smells Signed-off-by: Hongjing Chen <[email protected]> --------- Signed-off-by: Hongjing Chen <[email protected]>
- Loading branch information
1 parent
33a1f08
commit d1fa5d7
Showing
6 changed files
with
253 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,5 +60,5 @@ minio==7.0.2 | |
PyPDF2==1.26.0 | ||
reportlab==3.6.12 | ||
html-sanitizer==1.9.3 | ||
git+https://github.com/bcgov/[email protected].15#egg=registry_schemas | ||
git+https://github.com/bcgov/[email protected].16#egg=registry_schemas | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
git+https://github.com/bcgov/[email protected].15#egg=registry_schemas | ||
git+https://github.com/bcgov/[email protected].16#egg=registry_schemas |
164 changes: 164 additions & 0 deletions
164
legal-api/src/legal_api/services/filings/validations/agm_extension.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
# Copyright © 2023 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. | ||
"""Validation for the Agm Extension filing.""" | ||
from http import HTTPStatus | ||
from typing import Dict, Optional | ||
|
||
from dateutil.relativedelta import relativedelta | ||
from flask_babel import _ as babel # noqa: N813, I004, I001; importing camelcase '_' as a name | ||
# noqa: I003 | ||
from legal_api.errors import Error | ||
from legal_api.models import Business | ||
from legal_api.utils.legislation_datetime import LegislationDatetime | ||
from ...utils import get_bool, get_int, get_str # noqa: I003 | ||
# noqa: I003 | ||
|
||
AGM_EXTENSION_PATH = '/filing/agmExtension' | ||
EXPIRED_ERROR = 'Allotted period to request extension has expired.' | ||
GRANT_FAILURE = 'Fail to grant extension.' | ||
|
||
|
||
def validate(business: Business, filing: Dict) -> Optional[Error]: | ||
"""Validate the AGM Extension filing.""" | ||
if not business or not filing: | ||
return Error(HTTPStatus.BAD_REQUEST, [{'error': babel('A valid business and filing are required.')}]) | ||
|
||
msg = [] | ||
|
||
is_first_agm = get_bool(filing, f'{AGM_EXTENSION_PATH}/isFirstAgm') | ||
|
||
if is_first_agm: | ||
msg.extend(first_agm_validation(business, filing)) | ||
else: | ||
msg.extend(subsequent_agm_validation(filing)) | ||
|
||
if msg: | ||
return Error(HTTPStatus.BAD_REQUEST, msg) | ||
|
||
return None | ||
|
||
|
||
def first_agm_validation(business: Business, filing: Dict) -> list: | ||
"""Validate filing for first AGM Extension.""" | ||
msg = [] | ||
|
||
has_ext_req_for_agm_year = get_bool(filing, f'{AGM_EXTENSION_PATH}/extReqForAgmYear') | ||
founding_date = LegislationDatetime.as_legislation_timezone_from_date(business.founding_date).date() | ||
|
||
if not has_ext_req_for_agm_year: | ||
# first AGM, first extension | ||
now = LegislationDatetime.datenow() | ||
latest_ext_date = founding_date + relativedelta(months=18, days=5) | ||
if now > latest_ext_date: | ||
msg.append({'error': EXPIRED_ERROR, | ||
'path': f'{AGM_EXTENSION_PATH}/isFirstAgm'}) | ||
else: | ||
total_approved_ext = get_int(filing, f'{AGM_EXTENSION_PATH}/totalApprovedExt') | ||
extension_duration = get_int(filing, f'{AGM_EXTENSION_PATH}/extensionDuration') | ||
if total_approved_ext != 6 or extension_duration != 6: | ||
msg.append({'error': babel(GRANT_FAILURE)}) | ||
else: | ||
# first AGM, second extension or more | ||
if not (curr_ext_expire_date_str := get_str(filing, f'{AGM_EXTENSION_PATH}/expireDateCurrExt')): | ||
return [{'error': 'Expiry date for current extension is required.', | ||
'path': f'{AGM_EXTENSION_PATH}/expireDateCurrExt'}] | ||
|
||
curr_ext_expire_date =\ | ||
LegislationDatetime.as_legislation_timezone_from_date_str(curr_ext_expire_date_str).date() | ||
allowable_ext_date = founding_date + relativedelta(months=30) | ||
now = LegislationDatetime.datenow() | ||
if curr_ext_expire_date >= allowable_ext_date: | ||
msg.append({'error': 'Company has received the maximum 12 months of allowable extensions.', | ||
'path': f'{AGM_EXTENSION_PATH}/expireDateCurrExt'}) | ||
elif now > curr_ext_expire_date + relativedelta(days=5): | ||
msg.append({'error': EXPIRED_ERROR, | ||
'path': f'{AGM_EXTENSION_PATH}/expireDateCurrExt'}) | ||
else: | ||
total_approved_ext = get_int(filing, f'{AGM_EXTENSION_PATH}/totalApprovedExt') | ||
extension_duration = get_int(filing, f'{AGM_EXTENSION_PATH}/extensionDuration') | ||
|
||
baseline = founding_date + relativedelta(months=18) | ||
expected_total_approved_ext, expected_extension_duration =\ | ||
_calculate_granted_ext(curr_ext_expire_date, baseline) | ||
|
||
if expected_total_approved_ext != total_approved_ext or\ | ||
expected_extension_duration != extension_duration: | ||
msg.append({'error': babel(GRANT_FAILURE)}) | ||
|
||
return msg | ||
|
||
|
||
def subsequent_agm_validation(filing: Dict) -> list: | ||
"""Validate filing for subsequent AGM Extension.""" | ||
msg = [] | ||
|
||
has_ext_req_for_agm_year = filing['filing']['agmExtension']['extReqForAgmYear'] | ||
if not (prev_agm_ref_date_str := get_str(filing, f'{AGM_EXTENSION_PATH}/prevAgmRefDate')): | ||
return [{'error': 'Previous AGM date or a reference date is required.', | ||
'path': f'{AGM_EXTENSION_PATH}/prevAgmRefDate'}] | ||
|
||
prev_agm_ref_date =\ | ||
LegislationDatetime.as_legislation_timezone_from_date_str(prev_agm_ref_date_str).date() | ||
|
||
if not has_ext_req_for_agm_year: | ||
# subsequent AGM, first extension | ||
now = LegislationDatetime.datenow() | ||
latest_ext_date = prev_agm_ref_date + relativedelta(months=15, days=5) | ||
if now > latest_ext_date: | ||
msg.append({'error': EXPIRED_ERROR, | ||
'path': f'{AGM_EXTENSION_PATH}/prevAgmRefDate'}) | ||
else: | ||
total_approved_ext = get_int(filing, f'{AGM_EXTENSION_PATH}/totalApprovedExt') | ||
extension_duration = get_int(filing, f'{AGM_EXTENSION_PATH}/extensionDuration') | ||
if total_approved_ext != 6 or extension_duration != 6: | ||
msg.append({'error': babel(GRANT_FAILURE)}) | ||
else: | ||
# subsequent AGM, second extension or more | ||
if not (curr_ext_expire_date_str := get_str(filing, f'{AGM_EXTENSION_PATH}/expireDateCurrExt')): | ||
return [{'error': 'Expiry date for current extension is required.', | ||
'path': f'{AGM_EXTENSION_PATH}/expireDateCurrExt'}] | ||
|
||
curr_ext_expire_date =\ | ||
LegislationDatetime.as_legislation_timezone_from_date_str(curr_ext_expire_date_str).date() | ||
|
||
allowable_ext_date = prev_agm_ref_date + relativedelta(months=12) | ||
now = LegislationDatetime.datenow() | ||
|
||
if curr_ext_expire_date >= allowable_ext_date: | ||
msg.append({'error': 'Company has received the maximum 12 months of allowable extensions.', | ||
'path': f'{AGM_EXTENSION_PATH}/expireDateCurrExt'}) | ||
elif now > curr_ext_expire_date + relativedelta(days=5): | ||
msg.append({'error': EXPIRED_ERROR, | ||
'path': f'{AGM_EXTENSION_PATH}/expireDateCurrExt'}) | ||
else: | ||
total_approved_ext = get_int(filing, f'{AGM_EXTENSION_PATH}/totalApprovedExt') | ||
extension_duration = get_int(filing, f'{AGM_EXTENSION_PATH}/extensionDuration') | ||
|
||
expected_total_approved_ext, expected_extension_duration =\ | ||
_calculate_granted_ext(curr_ext_expire_date, prev_agm_ref_date) | ||
|
||
if expected_total_approved_ext != total_approved_ext or\ | ||
expected_extension_duration != extension_duration: | ||
msg.append({'error': babel(GRANT_FAILURE)}) | ||
|
||
return msg | ||
|
||
|
||
def _calculate_granted_ext(curr_ext_expire_date, baseline_date) -> tuple: | ||
"""Calculate expected total approved extension and extension duration.""" | ||
total_approved_ext = relativedelta(curr_ext_expire_date, baseline_date).months | ||
extension_duration = min(12-total_approved_ext, 6) | ||
total_approved_ext += extension_duration | ||
|
||
return total_approved_ext, extension_duration |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
legal-api/tests/unit/services/filings/validations/test_agm_extension.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
# Copyright © 2023 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 AGM Extension is validated correctly.""" | ||
import copy | ||
from dateutil.relativedelta import relativedelta | ||
from http import HTTPStatus | ||
from unittest.mock import patch | ||
|
||
import pytest | ||
from registry_schemas.example_data import FILING_HEADER | ||
|
||
from legal_api.services.filings.validations.validation import validate | ||
from legal_api.utils.legislation_datetime import LegislationDatetime | ||
from legal_api.utils.datetime import datetime | ||
|
||
from tests.unit.models import factory_business | ||
|
||
|
||
@pytest.mark.parametrize( | ||
'test_name, founding_date, agm_ext_json, expected_code, message', | ||
[ | ||
('SUCCESS_FIRST_AGM_FIRST_EXT', '2023-10-01', | ||
{'year': '2023','isFirstAgm': True, 'extReqForAgmYear': False, 'totalApprovedExt': 6, 'extensionDuration': 6}, | ||
None, None), | ||
('FAIL_FIRST_AGM_FIRST_EXT_TOO_LATE', '2020-10-01', {'year': '2023','isFirstAgm': True, 'extReqForAgmYear': False}, | ||
HTTPStatus.BAD_REQUEST, 'Allotted period to request extension has expired.'), | ||
('SUCCESS_FIRST_AGM_MORE_EXT', '2022-10-01', | ||
{'year': '2023','isFirstAgm': True, 'extReqForAgmYear': True, 'expireDateCurrExt': '2024-10-01', 'totalApprovedExt': 12, 'extensionDuration': 6}, | ||
None, None), | ||
('FAIL_FIRST_AGM_MORE_EXT_TOO_LATE', '2022-10-01', {'year': '2023','isFirstAgm': True, 'extReqForAgmYear': True, 'expireDateCurrExt': '2023-12-01'}, | ||
HTTPStatus.BAD_REQUEST, 'Allotted period to request extension has expired.'), | ||
('FAIL_FIRST_AGM_MORE_EXT_EXCEED_LIMIT', '2021-10-01', {'year': '2023','isFirstAgm': True, 'extReqForAgmYear': True, 'expireDateCurrExt': '2024-10-01'}, | ||
HTTPStatus.BAD_REQUEST, 'Company has received the maximum 12 months of allowable extensions.'), | ||
('SUCCESS_SUBSEQUENT_AGM_FIRST_EXT', '2020-10-01', | ||
{'year': '2023','isFirstAgm': False, 'extReqForAgmYear': False, 'prevAgmRefDate':'2023-10-01', 'totalApprovedExt': 6, 'extensionDuration': 6}, | ||
None, None), | ||
('FAIL_SUBSEQUENT_AGM_FIRST_EXT_TOO_LATE', '2020-10-01', {'year': '2023','isFirstAgm': False, 'extReqForAgmYear': False, 'prevAgmRefDate':'2022-06-01'}, | ||
HTTPStatus.BAD_REQUEST, 'Allotted period to request extension has expired.'), | ||
('SUCCESS_SUBSEQUENT_AGM_MORE_EXT', '2022-10-01', | ||
{'year': '2023','isFirstAgm': False, 'extReqForAgmYear': True, 'prevAgmRefDate':'2023-06-01', 'expireDateCurrExt':'2024-05-01', 'totalApprovedExt': 12, 'extensionDuration': 1}, | ||
None, None), | ||
('FAIL_SUBSEQUENT_AGM_MORE_EXT_TOO_LATE', '2022-10-01', | ||
{'year': '2023','isFirstAgm': False, 'extReqForAgmYear': True, 'prevAgmRefDate':'2023-06-01', 'expireDateCurrExt':'2023-12-01'}, | ||
HTTPStatus.BAD_REQUEST, 'Allotted period to request extension has expired.'), | ||
('FAIL_SUBSEQUENT_AGM_MORE_EXT_EXCEED_LIMIT', '2022-10-01', | ||
{'year': '2023','isFirstAgm': False, 'extReqForAgmYear': True, 'prevAgmRefDate':'2023-06-01', 'expireDateCurrExt':'2024-06-01'}, | ||
HTTPStatus.BAD_REQUEST, 'Company has received the maximum 12 months of allowable extensions.') | ||
] | ||
) | ||
def test_validate_agm_extension(session, mocker, test_name, founding_date, agm_ext_json, expected_code, message): | ||
"""Assert validate AGM extension.""" | ||
business = factory_business( | ||
identifier='BC1234567', entity_type='BC', founding_date=LegislationDatetime.as_legislation_timezone_from_date_str(founding_date) | ||
) | ||
filing = copy.deepcopy(FILING_HEADER) | ||
filing['filing']['agmExtension'] = agm_ext_json | ||
filing['filing']['header']['name'] = 'agmExtension' | ||
|
||
with patch.object(LegislationDatetime, 'now', return_value=LegislationDatetime.as_legislation_timezone_from_date_str('2024-01-01')): | ||
err = validate(business, filing) | ||
|
||
if not test_name.startswith('SUCCESS'): | ||
assert expected_code == err.code | ||
if message: | ||
assert message == err.msg[0]['error'] | ||
else: | ||
assert not err |