Skip to content

Commit

Permalink
18053 Legal API - Validation (AGM Extension) (#2298)
Browse files Browse the repository at this point in the history
* 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
chenhongjing authored Nov 15, 2023
1 parent 33a1f08 commit d1fa5d7
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 2 deletions.
2 changes: 1 addition & 1 deletion legal-api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

2 changes: 1 addition & 1 deletion legal-api/requirements/bcregistry-libraries.txt
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 legal-api/src/legal_api/services/filings/validations/agm_extension.py
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from legal_api.services.utils import get_str

from .admin_freeze import validate as admin_freeze_validate
from .agm_extension import validate as agm_extension_validate
from .agm_location_change import validate as agm_location_change_validate
from .alteration import validate as alteration_validate
from .annual_report import validate as annual_report_validate
Expand Down Expand Up @@ -183,6 +184,9 @@ def validate(business: Business, filing_json: Dict) -> Error: # pylint: disable
elif k == Filing.FILINGS['agmLocationChange'].get('name'):
err = agm_location_change_validate(business, filing_json)

elif k == Filing.FILINGS['agmExtension'].get('name'):
err = agm_extension_validate(business, filing_json)

if err:
return err

Expand Down
5 changes: 5 additions & 0 deletions legal-api/src/legal_api/utils/legislation_datetime.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ def now() -> datetime:
"""Construct a datetime using the legislation timezone."""
return datetime.now().astimezone(pytz.timezone(current_app.config.get('LEGISLATIVE_TIMEZONE')))

@staticmethod
def datenow() -> date:
"""Construct a date using the legislation timezone."""
return LegislationDatetime.now().date()

@staticmethod
def tomorrow_midnight() -> datetime:
"""Construct a datetime tomorrow midnight using the legislation timezone."""
Expand Down
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

0 comments on commit d1fa5d7

Please sign in to comment.