diff --git a/legal-api/requirements.txt b/legal-api/requirements.txt index edf4fed38..deee8bf03 100755 --- a/legal-api/requirements.txt +++ b/legal-api/requirements.txt @@ -59,4 +59,4 @@ PyPDF2==1.26.0 reportlab==3.6.12 html-sanitizer==2.4.1 lxml==5.2.2 -git+https://github.com/bcgov/business-schemas.git@2.18.28#egg=registry_schemas +git+https://github.com/bcgov/business-schemas.git@2.18.30#egg=registry_schemas diff --git a/legal-api/src/legal_api/services/filings/validations/notice_of_withdrawal.py b/legal-api/src/legal_api/services/filings/validations/notice_of_withdrawal.py new file mode 100644 index 000000000..443bf4bc7 --- /dev/null +++ b/legal-api/src/legal_api/services/filings/validations/notice_of_withdrawal.py @@ -0,0 +1,72 @@ +# 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. +"""Validation for the Notice of Withdrawal filing.""" +from http import HTTPStatus +from typing import Dict, Final, Optional + +from flask_babel import _ as babel # noqa: N813, I004, I001, I003; + +from legal_api.errors import Error +from legal_api.models import Filing +from legal_api.models.db import db # noqa: I001 +from legal_api.services.utils import get_int +from legal_api.utils.datetime import datetime as dt + + +def validate(filing: Dict) -> Optional[Error]: + """Validate the Notice of Withdrawal filing.""" + if not filing: + return Error(HTTPStatus.BAD_REQUEST, [{'error': babel('A valid filing is required.')}]) + + msg = [] + + withdrawn_filing_id_path: Final = '/filing/noticeOfWithdrawal/filingId' + withdrawn_filing_id = get_int(filing, withdrawn_filing_id_path) + if not withdrawn_filing_id: + msg.append({'error': babel('Filing Id is required.'), 'path': withdrawn_filing_id_path}) + return msg # cannot continue validation without the to be withdrawn filing id + + err = validate_withdrawn_filing(withdrawn_filing_id) + if err: + msg.extend(err) + + if msg: + return Error(HTTPStatus.BAD_REQUEST, msg) + return None + + +def validate_withdrawn_filing(withdrawn_filing_id: int): + """Validate the to be withdrawn filing id exists, the filing has a FED, the filing status is PAID.""" + msg = [] + # check whether the filing ID exists + withdrawn_filing = db.session.query(Filing). \ + filter(Filing.id == withdrawn_filing_id).one_or_none() + if not withdrawn_filing: + msg.append({'error': babel('The filing to be withdrawn cannot be found.')}) + return msg # cannot continue if the withdrawn filing doesn't exist + + # check whether the filing has a Future Effective Date(FED) + now = dt.utcnow() + filing_effective_date = dt.fromisoformat(str(withdrawn_filing.effective_date)) + if filing_effective_date < now: + msg.append({'error': babel('Only filings with a future effective date can be withdrawn.')}) + + # check the filing status + filing_status = withdrawn_filing.status + if filing_status != Filing.Status.PAID.value: + msg.append({'error': babel('Only paid filings with a future effective date can be withdrawn.')}) + + if msg: + return msg + return None diff --git a/legal-api/src/legal_api/services/filings/validations/validation.py b/legal-api/src/legal_api/services/filings/validations/validation.py index d2ea89650..cd0a85c33 100644 --- a/legal-api/src/legal_api/services/filings/validations/validation.py +++ b/legal-api/src/legal_api/services/filings/validations/validation.py @@ -40,6 +40,7 @@ from .dissolution import DissolutionTypes from .dissolution import validate as dissolution_validate from .incorporation_application import validate as incorporation_application_validate +from .notice_of_withdrawal import validate as notice_of_withdrawal_validate from .put_back_on import validate as put_back_on_validate from .registrars_notation import validate as registrars_notation_validate from .registrars_order import validate as registrars_order_validate @@ -182,6 +183,9 @@ def validate(business: Business, # pylint: disable=too-many-branches,too-many-s elif k == Filing.FILINGS['continuationIn'].get('name'): err = continuation_in_validate(filing_json) + elif k == Filing.FILINGS['noticeOfWithdrawal'].get('name'): + err = notice_of_withdrawal_validate(filing_json) + if err: return err diff --git a/legal-api/tests/unit/services/filings/validations/test_notice_of_withdrawal.py b/legal-api/tests/unit/services/filings/validations/test_notice_of_withdrawal.py new file mode 100644 index 000000000..4020aea84 --- /dev/null +++ b/legal-api/tests/unit/services/filings/validations/test_notice_of_withdrawal.py @@ -0,0 +1,93 @@ +# 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 the Notice of Withdrawal filing is validated correctly.""" +import copy +from datetime import datetime, timedelta +from http import HTTPStatus + +import pytest + +from legal_api.models import Filing, RegistrationBootstrap +from legal_api.services.filings import validate +from legal_api.services.filings.validations.notice_of_withdrawal import ( + validate_withdrawn_filing, + validate as validate_in_notice_of_withdrawal +) +from tests.unit.models import factory_pending_filing, factory_business +from . import lists_are_equal + +from registry_schemas.example_data import FILING_HEADER, NOTICE_OF_WITHDRAWAL, DISSOLUTION, INCORPORATION + + +# setup +FILING_NOT_EXIST_MSG = {'error': 'The filing to be withdrawn cannot be found.'} +FILING_NOT_FED_MSG = {'error': 'Only filings with a future effective date can be withdrawn.'} +FILING_NOT_PAID_MSG = {'error': 'Only paid filings with a future effective date can be withdrawn.'} +MISSING_FILING_DICT_MSG = {'error': 'A valid filing is required.'} + + +# tests + +@pytest.mark.parametrize( + 'test_name, is_filing_exist, withdrawn_filing_status, is_future_effective, has_filing_id, expected_code, expected_msg',[ + ('EXIST_BUSINESS_SUCCESS', True, Filing.Status.PAID, True, True, None, None), + ('EXIST_BUSINESS_FAIL_NOT_PAID', True, Filing.Status.PENDING, True, True, HTTPStatus.BAD_REQUEST, [FILING_NOT_PAID_MSG]), + ('EXIST_BUSINESS_FAIL_NOT_FED', True, Filing.Status.PAID, False, True, HTTPStatus.BAD_REQUEST, [FILING_NOT_FED_MSG]), + ('EXIST_BUSINESS_FAIL_FILING_NOT_EXIST', False, Filing.Status.PAID, True, True, HTTPStatus.BAD_REQUEST, [FILING_NOT_EXIST_MSG]), + ('EXIST_BUSINESS_FAIL_MISS_FILING_ID', True, Filing.Status.PAID, True, False, HTTPStatus.UNPROCESSABLE_ENTITY, ''), + ('EXIST_BUSINESS_FAIL_NOT_PAID_NOT_FED', True, Filing.Status.PENDING, False, True, HTTPStatus.BAD_REQUEST, [FILING_NOT_FED_MSG, FILING_NOT_PAID_MSG]) + ] +) +def test_validate_notice_of_withdrawal(session, test_name, is_filing_exist, withdrawn_filing_status, is_future_effective, has_filing_id, expected_code, expected_msg): + """Assert that notice of withdrawal flings can be validated""" + today = datetime.utcnow().date() + future_effective_date = today + timedelta(days=5) + future_effective_date = future_effective_date.isoformat() + identifier = 'BC1234567' + business = factory_business(identifier) + # file a voluntary dissolution with a FED + if is_filing_exist: + withdrawn_json = copy.deepcopy(FILING_HEADER) + withdrawn_json['filing']['header']['name'] = 'dissolution' + withdrawn_json['filing']['business']['legalType'] = 'BC' + withdrawn_json['filing']['dissolution'] = copy.deepcopy(DISSOLUTION) + withdrawn_json['filing']['dissolution']['dissolutionDate'] = future_effective_date + withdrawn_filing = factory_pending_filing(business, withdrawn_json) + if is_filing_exist: + if is_future_effective: + withdrawn_filing.effective_date = future_effective_date + if withdrawn_filing_status == Filing.Status.PAID: + withdrawn_filing.payment_completion_date = datetime.utcnow().isoformat() + withdrawn_filing.save() + withdrawn_filing_id = withdrawn_filing.id + + # create a notice of withdrawal filing json + filing_json = copy.deepcopy(FILING_HEADER) + filing_json['filing']['header']['name'] = 'noticeOfWithdrawal' + filing_json['filing']['business']['legalType'] = 'BC' + filing_json['filing']['noticeOfWithdrawal'] = copy.deepcopy(NOTICE_OF_WITHDRAWAL) + if has_filing_id: + if is_filing_exist: + filing_json['filing']['noticeOfWithdrawal']['filingId'] = withdrawn_filing_id + else: + del filing_json['filing']['noticeOfWithdrawal']['filingId'] + + err = validate(business, filing_json) + if expected_code: + assert err.code == expected_code + if has_filing_id: # otherwise, won't pass schema validation, and the error msg will be very long + assert lists_are_equal(err.msg, expected_msg) + else: + assert err is None + \ No newline at end of file