From d2e74a336b28939709fda414172bbdd96b668bb2 Mon Sep 17 00:00:00 2001 From: Kial Jinnah Date: Tue, 10 Feb 2026 10:51:22 -0500 Subject: [PATCH 1/8] Business API - transition filing validation updates Signed-off-by: Kial Jinnah --- legal-api/poetry.lock | 20 +- legal-api/pyproject.toml | 4 +- .../validations/change_of_liquidators.py | 6 +- .../filings/validations/common_validations.py | 42 ++++- .../filings/validations/transition.py | 70 +++++++ .../filings/validations/validation.py | 4 + .../validations/test_common_validations.py | 109 ++++++++++- .../filings/validations/test_transition.py | 171 ++++++++++++++++++ 8 files changed, 410 insertions(+), 16 deletions(-) create mode 100644 legal-api/src/legal_api/services/filings/validations/transition.py create mode 100644 legal-api/tests/unit/services/filings/validations/test_transition.py diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 1fb69579a2..930b90223e 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "alembic" @@ -785,11 +785,11 @@ files = [ ] [package.dependencies] -google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" -google-auth = ">=1.25.0,<3.0dev" +google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0.dev0" +google-auth = ">=1.25.0,<3.0.dev0" [package.extras] -grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] +grpc = ["grpcio (>=1.38.0,<2.0.dev0)", "grpcio-status (>=1.38.0,<2.0.dev0)"] [[package]] name = "google-cloud-datastore" @@ -1225,7 +1225,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format\""} idna = {version = "*", optional = true, markers = "extra == \"format\""} isoduration = {version = "*", optional = true, markers = "extra == \"format\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} @@ -2165,7 +2165,11 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "registry_schemas" +<<<<<<< Updated upstream version = "2.18.59" +======= +version = "2.18.62" +>>>>>>> Stashed changes description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -2183,8 +2187,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.60" -resolved_reference = "4e355909b8d16237421bf7eb391acd23e037d437" +reference = "2.18.62" +resolved_reference = "1cea81cf14104fb7d4989169236eff13b3df16c4" [[package]] name = "reportlab" @@ -3086,4 +3090,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9.22,<3.10" -content-hash = "c256ff2667772a27ed6a47b64355b104f197a99bafe2b441679263fba0674bf4" +content-hash = "bbb350d1d3081b4743035ac827680e44de266ed4150b31e115f423ce492d9674" diff --git a/legal-api/pyproject.toml b/legal-api/pyproject.toml index 0894515d07..3d635e7276 100644 --- a/legal-api/pyproject.toml +++ b/legal-api/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "blinker (==1.4)", "pyjwt (==2.8.0)", - "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.60#egg=registry_schemas", + "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.62#egg=registry_schemas", "sql-versioning @ git+https://github.com/bcgov/lear.git@main#subdirectory=python/common/sql-versioning", "gcp-queue @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/gcp-queue", "structured-logging @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/structured-logging" @@ -174,7 +174,7 @@ minversion = "2.0" testpaths = [ "tests", ] -addopts = "--cov=src/legal_api" +# addopts = "--cov=src/legal_api" python_files = [ "test*.py" ] diff --git a/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py b/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py index 06d03e84a8..5f88a8922a 100644 --- a/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py +++ b/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py @@ -37,7 +37,7 @@ from legal_api.errors import Error from legal_api.models import Business, PartyRole -from legal_api.services.filings.validations.common_validations import validate_offices_addresses, validate_relationships +from legal_api.services.filings.validations.common_validations import validate_offices, validate_relationships def validate(business: Business, filing_json: dict) -> Optional[Error]: @@ -58,7 +58,9 @@ def validate(business: Business, filing_json: dict) -> Optional[Error]: )) if filing_json["filing"][filing_type].get("offices"): - msg.extend(validate_offices_addresses(filing_json, filing_type)) + allowed_offices = ['liquidationRecordsOffice'] if filing_sub_type in ["intentToLiquidate", "changeAddressLiquidator"] else [] + required_offices = ['liquidationRecordsOffice'] if filing_sub_type in ["intentToLiquidate"] else [] + msg.extend(validate_offices(filing_json, filing_type, allowed_offices, required_offices)) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) diff --git a/legal-api/src/legal_api/services/filings/validations/common_validations.py b/legal-api/src/legal_api/services/filings/validations/common_validations.py index 3a2f0db9d2..adec233c3a 100644 --- a/legal-api/src/legal_api/services/filings/validations/common_validations.py +++ b/legal-api/src/legal_api/services/filings/validations/common_validations.py @@ -519,7 +519,7 @@ def validate_relationships( # noqa: PLR0913 if identifier and not allow_edits: msg.append({"error": "Relationship edits are not allowed in this filing.", "path": f"{party_path}/{index}/entity"}) elif identifier and identifier not in party_ids: - msg.append({"error": "Relationship with this identifier does not exist.", "path": f"{party_path}/{index}/entity/identifier"}) + msg.append({"error": "Relationship with this identifier is not valid for this filing.", "path": f"{party_path}/{index}/entity/identifier"}) elif not identifier and not allow_new: msg.append({"error": "New Relationships are not allowed in this filing.", "path": f"{party_path}/{index}/entity"}) @@ -668,8 +668,28 @@ def validate_foreign_jurisdiction(foreign_jurisdiction: dict, return msg +def validate_offices(filing_json: dict, filing_type: str, allowed_types: list[str], required_types: list[str], bc_req: bool) -> list: + """Validate offices.""" + msg = [] + offices_dict: dict = filing_json["filing"][filing_type]["offices"] + offices_path = f"/filing/{filing_type}/offices" + for key, value in offices_dict.items(): + if key not in allowed_types: + msg.append({"error": f"Invalid office {key}. Only {allowed_types} are allowed.", + "path": f"/filing/{filing_type}/offices"}) + else: + + msg.extend(validate_addresses(value, f"{offices_path}/{key}", bc_req)) + + if missing_types := [office_type for office_type in required_types if office_type not in offices_dict.keys()]: + msg.append({"error": f"Missing required offices {missing_types}.", + "path": f"/filing/{filing_type}/offices"}) + return msg + + def validate_offices_addresses(filing_json: dict, filing_type: str) -> list: """Validate optional fields in office addresses.""" + # FUTURE: Update validations using this to use validate_offices instead msg = [] offices_dict = filing_json["filing"][filing_type]["offices"] offices_path = f"/filing/{filing_type}/offices" @@ -690,7 +710,8 @@ def validate_parties_addresses(filing_json: dict, filing_type: str, key: str = " def validate_addresses( addresses: dict, - addresses_path: str + addresses_path: str, + delivery_bc_req = False ) -> list: """Validate optional fields in addresses.""" msg = [] @@ -710,6 +731,23 @@ def validate_addresses( "error": _(f"{field} cannot start or end with whitespace."), "path": f"{address_type_path}/{field}" }) + + if delivery_bc_req and address_type == Address.JSON_DELIVERY: + region = address.get("addressRegion") + country = address["addressCountry"] + + if region != "BC": + msg.append({"error": "Address Region must be 'BC'.", + "path": addresses_path}) + + try: + country = pycountry.countries.search_fuzzy(country)[0].alpha_2 + if country != "CA": + raise LookupError + except LookupError: + msg.append({"error": "Address Country must be 'CA'.", + "path": addresses_path}) + return msg diff --git a/legal-api/src/legal_api/services/filings/validations/transition.py b/legal-api/src/legal_api/services/filings/validations/transition.py new file mode 100644 index 0000000000..1eef113a8d --- /dev/null +++ b/legal-api/src/legal_api/services/filings/validations/transition.py @@ -0,0 +1,70 @@ +# Copyright © 2025 Province of British Columbia +# +# Licensed under the BSD 3 Clause License, (the "License"); +# you may not use this file except in compliance with the License. +# The template for the license can be found here +# https://opensource.org/license/bsd-3-clause/ +# +# Redistribution and use in source and binary forms, +# with or without modification, are permitted provided that the +# following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +"""Validation for the Post Restoration Transition Application filing.""" +from http import HTTPStatus +from typing import Optional + +from legal_api.errors import Error +from legal_api.models import Business, PartyRole +from legal_api.services.filings.validations.common_validations import validate_offices, validate_relationships, validate_share_structure + + +def validate(business: Business, filing_json: dict) -> Optional[Error]: + """Validate the Post Restoration Transition Application filing.""" + filing_type = "transition" + + msg = [] + + msg.extend(validate_relationships(business, + filing_json, + filing_type, + PartyRole.RoleTypes.DIRECTOR, + False, + True)) + + office_types = ['registeredOffice', 'recordsOffice'] + msg.extend(validate_offices(filing_json, + filing_type, + office_types, + office_types, + True)) + + err = validate_share_structure(filing_json, filing_type, business.legal_type) + if err: + msg.extend(err) + + if msg: + return Error(HTTPStatus.BAD_REQUEST, 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 f0324a388c..f32317455b 100644 --- a/legal-api/src/legal_api/services/filings/validations/validation.py +++ b/legal-api/src/legal_api/services/filings/validations/validation.py @@ -57,6 +57,7 @@ from .restoration import validate as restoration_validate from .schemas import validate_against_schema from .special_resolution import validate as special_resolution_validate +from .transition import validate as transition_validate from .transparency_register import validate as transparency_register_validate @@ -229,6 +230,9 @@ def validate(business: Business, # noqa: PLR0915, PLR0912, PLR0911 elif k == Filing.FILINGS["putBackOff"].get("name"): err = put_back_off_validate(business, filing_json) + + elif k == Filing.FILINGS["transition"].get("name"): + err = transition_validate(business, filing_json) elif k == Filing.FILINGS["transparencyRegister"].get("name"): err = transparency_register_validate(filing_json) # pylint: disable=assignment-from-none diff --git a/legal-api/tests/unit/services/filings/validations/test_common_validations.py b/legal-api/tests/unit/services/filings/validations/test_common_validations.py index 31ed718198..abb05b1c3a 100644 --- a/legal-api/tests/unit/services/filings/validations/test_common_validations.py +++ b/legal-api/tests/unit/services/filings/validations/test_common_validations.py @@ -18,8 +18,7 @@ from unittest.mock import patch from legal_api.errors import Error -from legal_api.models.party import Party -from legal_api.models.party_role import PartyRole +from legal_api.models import Business, Party, PartyRole from legal_api.services import flags from legal_api.services.permissions import PermissionService import pytest @@ -53,6 +52,7 @@ validate_certified_by, validate_court_order, validate_email, + validate_offices, validate_offices_addresses, validate_parties_addresses, validate_party_name, @@ -102,6 +102,41 @@ 'addressCountry': 'CA' } +VALID_ADDRESS_BC = { + 'streetAddress': 'Valid street', + 'streetAddressAdditional': 'Suite 200', + 'addressCity': 'Vancouver', + 'addressRegion': 'BC', + 'postalCode': 'V6B 1A1', + 'addressCountry': 'CA' +} + +VALID_ADDRESS_ON = { + 'streetAddress': '88 Hawthorne', + 'addressCity': 'Ottawa', + 'addressRegion': 'ON', + 'postalCode': 'K1N 3H9', + 'addressCountry': 'CA' +} + +VALID_ADDRESS_EX_CA = { + 'streetAddress': 'Somewhere in the US', + 'addressCity': 'New York', + 'addressRegion': 'NY', + 'postalCode': '10001-1234', + 'addressCountry': 'US' +} + +VALID_OFFICE = { + 'deliveryAddress': VALID_ADDRESS_BC, + 'mailingAddress': VALID_ADDRESS_ON, +} + +VALID_OFFICE_EX_CA = { + 'deliveryAddress': VALID_ADDRESS_EX_CA, + 'mailingAddress': VALID_ADDRESS_EX_CA, +} + WHITESPACE_VALIDATED_ADDRESS_FIELDS = ( 'streetAddress', 'addressCity', @@ -110,6 +145,76 @@ ) +@pytest.mark.parametrize('test_name, allowed_types, required_types, bc_req, filing_office_data, expected_errs', [ + ('no_office_allowed_required_success', [], [], False, {}, []), + ('no_office_allowed_required_fail', + [], + [], + False, + {'recordsOffice': VALID_OFFICE}, + [{'error': 'Invalid office recordsOffice. Only [] are allowed.', 'path': '/filing/transition/offices'}]), + ('office_required_success', + ['registeredOffice'], + ['registeredOffice'], + False, + {'registeredOffice': VALID_OFFICE}, + []), + ('office_required_fail', + [], + ['registeredOffice'], + False, + {}, + [{'error': "Missing required offices ['registeredOffice'].", 'path': '/filing/transition/offices'}]), + ('multiple_office_allowed_success', + ['registeredOffice', 'recordsOffice'], + ['registeredOffice'], + False, + {'registeredOffice': VALID_OFFICE, 'recordsOffice': VALID_OFFICE}, + []), + ('multiple_office_allowed_fail', + ['registeredOffice', 'recordsOffice'], + ['registeredOffice'], + False, + {'registeredOffice': VALID_OFFICE, 'recordsOffice': VALID_OFFICE, 'liquidationRecordsOffice': VALID_OFFICE}, + [{'error': "Invalid office liquidationRecordsOffice. Only ['registeredOffice', 'recordsOffice'] are allowed.", 'path': '/filing/transition/offices'}]), + ('multiple_office_required_success', + ['registeredOffice', 'recordsOffice'], + ['registeredOffice', 'recordsOffice'], + False, + {'registeredOffice': VALID_OFFICE, 'recordsOffice': VALID_OFFICE}, + []), + ('multiple_office_required_fail', + ['registeredOffice', 'recordsOffice'], + ['registeredOffice', 'recordsOffice'], + False, + {'registeredOffice': VALID_OFFICE}, + [{'error': "Missing required offices ['recordsOffice'].", 'path': '/filing/transition/offices'}]), + ('office_bc_req_success', + ['registeredOffice', 'recordsOffice'], + ['registeredOffice'], + True, + {'registeredOffice': VALID_OFFICE}, + []), + ('office_bc_req_fail', + ['registeredOffice', 'recordsOffice'], + ['registeredOffice'], + True, + {'registeredOffice': VALID_OFFICE_EX_CA}, + [{'error': "Address Region must be 'BC'.", 'path': '/filing/transition/offices/registeredOffice'}, + {'error': "Address Country must be 'CA'.", 'path': '/filing/transition/offices/registeredOffice'}]), +]) +def test_validate_offices(session, test_name, allowed_types, required_types, bc_req, filing_office_data, expected_errs): + """Test offices can be validated as expected.""" + filing = copy.deepcopy(FILING_HEADER) + # NOTE: filing type could be anything for the purposes of this test + filing_type = 'transition' + filing['filing']['header']['name'] = filing_type + filing['filing'][filing_type] = {'offices': copy.deepcopy(filing_office_data)} + + errs = validate_offices(filing, filing_type, allowed_types, required_types, bc_req) + assert errs == expected_errs + + @pytest.mark.parametrize('filing_type, filing_data, office_type', [ ('amaglamationApplication', AMALGAMATION_APPLICATION, 'registeredOffice'), ('changeOfAddress', CHANGE_OF_ADDRESS, 'registeredOffice'), diff --git a/legal-api/tests/unit/services/filings/validations/test_transition.py b/legal-api/tests/unit/services/filings/validations/test_transition.py new file mode 100644 index 0000000000..1108ee425d --- /dev/null +++ b/legal-api/tests/unit/services/filings/validations/test_transition.py @@ -0,0 +1,171 @@ +# Copyright © 2025 Province of British Columbia +# +# Licensed under the BSD 3 Clause License, (the "License"); +# you may not use this file except in compliance with the License. +# The template for the license can be found here +# https://opensource.org/license/bsd-3-clause/ +# +# Redistribution and use in source and binary forms, +# with or without modification, are permitted provided that the +# following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +"""Test Post Restoration Transtion Application validation.""" +import copy +from datetime import date + +import datedelta +import pytest +from registry_schemas.example_data import TRANSITION_FILING_TEMPLATE + +from legal_api.models import Address, Business, PartyRole +from legal_api.services.filings.validations.transition import validate +from tests.unit.models import factory_business, factory_party_role +from tests.unit.services.warnings import factory_address +from .test_common_validations import INVALID_ADDRESS_NO_POSTAL_CODE, VALID_ADDRESS_BC, VALID_OFFICE, VALID_OFFICE_EX_CA + + +VALID_OFFICES = {'registeredOffice': VALID_OFFICE,'recordsOffice': VALID_OFFICE} +INVALID_OFFICES_EX_CA = {'registeredOffice': VALID_OFFICE_EX_CA, 'recordsOffice': VALID_OFFICE_EX_CA} +INVALID_OFFICES_NO_REGISTERED = {'recordsOffice': VALID_OFFICE} +INVALID_OFFICES_NO_RECORDS = {'registeredOffice': VALID_OFFICE} + +VALID_RELATIONSHIP = {'has_existing_id': True, 'valid_id': True, 'valid_address': True} +INVALID_RELATIONSHIP_NEW = {'has_existing_id': False, 'valid_id': False, 'valid_address': True} +INVALID_RELATIONSHIP_ID_MATCH = {'has_existing_id': True, 'valid_id': False, 'valid_address': True} +INVALID_RELATIONSHIP_ADDRESS = {'has_existing_id': True, 'valid_id': True, 'valid_address': False} + +VALID_SHARE = {'has_rights_restrictions': False, 'has_series': False} +VALID_SHARE_SERIES = {'has_rights_restrictions': True, 'has_series': True} +INVALID_SHARE_SERIES = {'has_rights_restrictions': False, 'has_series': True} + + +@pytest.mark.parametrize('test_name, offices, relationship, share, expected_errs', [ + ('Valid', VALID_OFFICES, VALID_RELATIONSHIP, VALID_SHARE, None), + ('Valid_series', VALID_OFFICES, VALID_RELATIONSHIP, VALID_SHARE_SERIES, None), + ('Invalid_office_ex_ca', + INVALID_OFFICES_EX_CA, + VALID_RELATIONSHIP, + VALID_SHARE, + [{'error': "Address Region must be 'BC'.", 'path': '/filing/transition/offices/registeredOffice'}, + {'error': "Address Country must be 'CA'.", 'path': '/filing/transition/offices/registeredOffice'}, + {'error': "Address Region must be 'BC'.", 'path': '/filing/transition/offices/recordsOffice'}, + {'error': "Address Country must be 'CA'.", 'path': '/filing/transition/offices/recordsOffice'}]), + ('Invalid_office_no_registered', + INVALID_OFFICES_NO_REGISTERED, + VALID_RELATIONSHIP, + VALID_SHARE, + [{'error': "Missing required offices ['registeredOffice'].", 'path': '/filing/transition/offices'}]), + ('Invalid_office_no_records', + INVALID_OFFICES_NO_RECORDS, + VALID_RELATIONSHIP, + VALID_SHARE, + [{'error': "Missing required offices ['recordsOffice'].", 'path': '/filing/transition/offices'}]), + ('Invalid_relationship_new', + VALID_OFFICES, + INVALID_RELATIONSHIP_NEW, + VALID_SHARE, + [{'error': 'New Relationships are not allowed in this filing.', 'path': '/filing/transition/relationships/0/entity'}]), + ('Invalid_relationship_wrong_id', + VALID_OFFICES, + INVALID_RELATIONSHIP_ID_MATCH, + VALID_SHARE, + [{'error': 'Relationship with this identifier is not valid for this filing.', 'path': '/filing/transition/relationships/0/entity/identifier'}]), + ('Invalid_relationship_address', + VALID_OFFICES, + INVALID_RELATIONSHIP_ADDRESS, + VALID_SHARE, + [{'error': 'Postal code is required.', 'path': '/filing/transition/relationships/0/deliveryAddress/postalCode'}]), + ('Invalid_shares_series', + VALID_OFFICES, + VALID_RELATIONSHIP, + INVALID_SHARE_SERIES, + [{'error': 'Share class Class 1 Shares cannot have series when hasRightsOrRestrictions is false', 'path': '/filing/transition/shareClasses/0/series/'}]), +]) +def test_validate_transition(session, test_name, offices, relationship, share, expected_errs): + """Assert that a transition application can be validated.""" + # setup + now = date.today() + identifier = 'BC1234567' + founding_date = now - datedelta.YEAR + business: Business = factory_business(identifier, founding_date, founding_date, Business.LegalTypes.COMP) + + filing = copy.deepcopy(TRANSITION_FILING_TEMPLATE) + filing['filing']['business']['identifier'] = identifier + transition = filing['filing']['transition'] + + transition['offices'] = offices + + filing_relationship = transition['relationships'][0] + if relationship['valid_address']: + filing_relationship['deliveryAddress'] = VALID_ADDRESS_BC + filing_relationship['mailingAddress'] = VALID_ADDRESS_BC + else: + filing_relationship['deliveryAddress'] = INVALID_ADDRESS_NO_POSTAL_CODE + filing_relationship['mailingAddress'] = VALID_ADDRESS_BC + + if relationship['has_existing_id']: + officer_dict = { + 'firstName': 'Test', + 'lastName': 'Tester', + 'middleInitial': '', + 'partyType': 'person', + 'organizationName': '' + } + role: PartyRole = factory_party_role( + Address.create_address(VALID_ADDRESS_BC), + Address.create_address(VALID_ADDRESS_BC), + officer_dict, + founding_date, + None, + PartyRole.RoleTypes.DIRECTOR) + + business.party_roles.append(role) + business.save() + + filing_relationship['entity']['identifier'] = str(role.id if relationship['valid_id'] else role.id + 1) + + transition['relationships'] = [filing_relationship] + + filing_share = transition['shareStructure']['shareClasses'][0] + filing_share['hasRightsOrRestrictions'] = share['has_rights_restrictions'] + if share['has_series']: + filing_share['series'] = [{ + 'name': 'Series 1 Shares', + 'priority': 1, + 'hasMaximumShares': True, + 'maxNumberOfShares': 50, + 'hasRightsOrRestrictions': False, + }] + else: + filing_share['series'] = [] + + transition['shareStructure']['shareClasses'] = [filing_share] + # perform test + print(transition['offices']) + err = validate(business, filing) + if err: + print(test_name, err.msg) + + assert (not err and not expected_errs) or err.msg == expected_errs From 259300b44ecc07f9e7dbb61426071819b5b0e73a Mon Sep 17 00:00:00 2001 From: Kial Jinnah Date: Tue, 10 Feb 2026 10:54:05 -0500 Subject: [PATCH 2/8] cleanup Signed-off-by: Kial Jinnah --- legal-api/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legal-api/pyproject.toml b/legal-api/pyproject.toml index 3d635e7276..0595c9a609 100644 --- a/legal-api/pyproject.toml +++ b/legal-api/pyproject.toml @@ -174,7 +174,7 @@ minversion = "2.0" testpaths = [ "tests", ] -# addopts = "--cov=src/legal_api" +addopts = "--cov=src/legal_api" python_files = [ "test*.py" ] From 9d85c575b7150d519dae3a055c689646eaebd4ef Mon Sep 17 00:00:00 2001 From: Kial Jinnah Date: Tue, 10 Feb 2026 10:56:25 -0500 Subject: [PATCH 3/8] cleanup Signed-off-by: Kial Jinnah --- legal-api/poetry.lock | 4 ---- 1 file changed, 4 deletions(-) diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 930b90223e..ccb4c87051 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -2165,11 +2165,7 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "registry_schemas" -<<<<<<< Updated upstream -version = "2.18.59" -======= version = "2.18.62" ->>>>>>> Stashed changes description = "A short description of the project" optional = false python-versions = ">=3.6" From 08b52cf290585ebf884057fe62064584eacc4fb2 Mon Sep 17 00:00:00 2001 From: Kial Jinnah Date: Tue, 10 Feb 2026 10:57:37 -0500 Subject: [PATCH 4/8] cleanup Signed-off-by: Kial Jinnah --- .../tests/unit/services/filings/validations/test_transition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legal-api/tests/unit/services/filings/validations/test_transition.py b/legal-api/tests/unit/services/filings/validations/test_transition.py index 1108ee425d..e45aa73c62 100644 --- a/legal-api/tests/unit/services/filings/validations/test_transition.py +++ b/legal-api/tests/unit/services/filings/validations/test_transition.py @@ -162,8 +162,8 @@ def test_validate_transition(session, test_name, offices, relationship, share, e filing_share['series'] = [] transition['shareStructure']['shareClasses'] = [filing_share] + # perform test - print(transition['offices']) err = validate(business, filing) if err: print(test_name, err.msg) From 30e327b2d97ec5b2b815051e7eaabb0b1e09bf42 Mon Sep 17 00:00:00 2001 From: Kial Jinnah Date: Tue, 10 Feb 2026 11:04:43 -0500 Subject: [PATCH 5/8] chore: ruff fixes Signed-off-by: Kial Jinnah --- .../services/filings/validations/change_of_liquidators.py | 4 ++-- .../services/filings/validations/common_validations.py | 2 +- .../legal_api/services/filings/validations/transition.py | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py b/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py index 5f88a8922a..b70bbd9327 100644 --- a/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py +++ b/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py @@ -58,8 +58,8 @@ def validate(business: Business, filing_json: dict) -> Optional[Error]: )) if filing_json["filing"][filing_type].get("offices"): - allowed_offices = ['liquidationRecordsOffice'] if filing_sub_type in ["intentToLiquidate", "changeAddressLiquidator"] else [] - required_offices = ['liquidationRecordsOffice'] if filing_sub_type in ["intentToLiquidate"] else [] + allowed_offices = ["liquidationRecordsOffice"] if filing_sub_type in ["intentToLiquidate", "changeAddressLiquidator"] else [] + required_offices = ["liquidationRecordsOffice"] if filing_sub_type in ["intentToLiquidate"] else [] msg.extend(validate_offices(filing_json, filing_type, allowed_offices, required_offices)) if msg: diff --git a/legal-api/src/legal_api/services/filings/validations/common_validations.py b/legal-api/src/legal_api/services/filings/validations/common_validations.py index adec233c3a..b62165bb02 100644 --- a/legal-api/src/legal_api/services/filings/validations/common_validations.py +++ b/legal-api/src/legal_api/services/filings/validations/common_validations.py @@ -681,7 +681,7 @@ def validate_offices(filing_json: dict, filing_type: str, allowed_types: list[st msg.extend(validate_addresses(value, f"{offices_path}/{key}", bc_req)) - if missing_types := [office_type for office_type in required_types if office_type not in offices_dict.keys()]: + if missing_types := [office_type for office_type in required_types if office_type not in offices_dict]: msg.append({"error": f"Missing required offices {missing_types}.", "path": f"/filing/{filing_type}/offices"}) return msg diff --git a/legal-api/src/legal_api/services/filings/validations/transition.py b/legal-api/src/legal_api/services/filings/validations/transition.py index 1eef113a8d..6ca9cc08e0 100644 --- a/legal-api/src/legal_api/services/filings/validations/transition.py +++ b/legal-api/src/legal_api/services/filings/validations/transition.py @@ -37,7 +37,11 @@ from legal_api.errors import Error from legal_api.models import Business, PartyRole -from legal_api.services.filings.validations.common_validations import validate_offices, validate_relationships, validate_share_structure +from legal_api.services.filings.validations.common_validations import ( + validate_offices, + validate_relationships, + validate_share_structure, +) def validate(business: Business, filing_json: dict) -> Optional[Error]: @@ -53,7 +57,7 @@ def validate(business: Business, filing_json: dict) -> Optional[Error]: False, True)) - office_types = ['registeredOffice', 'recordsOffice'] + office_types = ["registeredOffice", "recordsOffice"] msg.extend(validate_offices(filing_json, filing_type, office_types, From 3cdbc8ebb004cd015020c5b17ea26f5578a563a7 Mon Sep 17 00:00:00 2001 From: Kial Jinnah Date: Tue, 10 Feb 2026 14:30:03 -0500 Subject: [PATCH 6/8] chore: fix col Signed-off-by: Kial Jinnah --- .../services/filings/validations/change_of_liquidators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py b/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py index b70bbd9327..6b74dc91fe 100644 --- a/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py +++ b/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py @@ -60,7 +60,7 @@ def validate(business: Business, filing_json: dict) -> Optional[Error]: if filing_json["filing"][filing_type].get("offices"): allowed_offices = ["liquidationRecordsOffice"] if filing_sub_type in ["intentToLiquidate", "changeAddressLiquidator"] else [] required_offices = ["liquidationRecordsOffice"] if filing_sub_type in ["intentToLiquidate"] else [] - msg.extend(validate_offices(filing_json, filing_type, allowed_offices, required_offices)) + msg.extend(validate_offices(filing_json, filing_type, allowed_offices, required_offices, False)) if msg: return Error(HTTPStatus.BAD_REQUEST, msg) From bd4615d4374036563cc2be7aa0c18b73d35e2574 Mon Sep 17 00:00:00 2001 From: Kial Jinnah Date: Tue, 10 Feb 2026 15:12:34 -0500 Subject: [PATCH 7/8] chore: add prints to figure out CI only err --- .../unit/services/filings/validations/test_transition.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/legal-api/tests/unit/services/filings/validations/test_transition.py b/legal-api/tests/unit/services/filings/validations/test_transition.py index e45aa73c62..7673431958 100644 --- a/legal-api/tests/unit/services/filings/validations/test_transition.py +++ b/legal-api/tests/unit/services/filings/validations/test_transition.py @@ -125,6 +125,7 @@ def test_validate_transition(session, test_name, offices, relationship, share, e filing_relationship['deliveryAddress'] = INVALID_ADDRESS_NO_POSTAL_CODE filing_relationship['mailingAddress'] = VALID_ADDRESS_BC + print(relationship) if relationship['has_existing_id']: officer_dict = { 'firstName': 'Test', @@ -140,11 +141,13 @@ def test_validate_transition(session, test_name, offices, relationship, share, e founding_date, None, PartyRole.RoleTypes.DIRECTOR) - + print(role) business.party_roles.append(role) business.save() - + print(business.party_roles) filing_relationship['entity']['identifier'] = str(role.id if relationship['valid_id'] else role.id + 1) + print(role.id) + print(filing_relationship['entity']['identifier']) transition['relationships'] = [filing_relationship] From b4f2688424113269ec71b7dea9e4a7a8826e7d0a Mon Sep 17 00:00:00 2001 From: Kial Jinnah Date: Wed, 11 Feb 2026 09:00:24 -0500 Subject: [PATCH 8/8] chore: tests fixed Signed-off-by: Kial Jinnah --- .../filings/validations/test_transition.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/legal-api/tests/unit/services/filings/validations/test_transition.py b/legal-api/tests/unit/services/filings/validations/test_transition.py index 7673431958..2c4f76ac82 100644 --- a/legal-api/tests/unit/services/filings/validations/test_transition.py +++ b/legal-api/tests/unit/services/filings/validations/test_transition.py @@ -33,6 +33,7 @@ # POSSIBILITY OF SUCH DAMAGE. """Test Post Restoration Transtion Application validation.""" import copy +import random from datetime import date import datedelta @@ -107,11 +108,12 @@ def test_validate_transition(session, test_name, offices, relationship, share, e """Assert that a transition application can be validated.""" # setup now = date.today() - identifier = 'BC1234567' + identifier = f'BC{random.randint(1000000, 9999999)}' founding_date = now - datedelta.YEAR business: Business = factory_business(identifier, founding_date, founding_date, Business.LegalTypes.COMP) filing = copy.deepcopy(TRANSITION_FILING_TEMPLATE) + filing['filing']['header']['date'] = now.isoformat() filing['filing']['business']['identifier'] = identifier transition = filing['filing']['transition'] @@ -125,7 +127,6 @@ def test_validate_transition(session, test_name, offices, relationship, share, e filing_relationship['deliveryAddress'] = INVALID_ADDRESS_NO_POSTAL_CODE filing_relationship['mailingAddress'] = VALID_ADDRESS_BC - print(relationship) if relationship['has_existing_id']: officer_dict = { 'firstName': 'Test', @@ -138,16 +139,13 @@ def test_validate_transition(session, test_name, offices, relationship, share, e Address.create_address(VALID_ADDRESS_BC), Address.create_address(VALID_ADDRESS_BC), officer_dict, - founding_date, + founding_date.isoformat(), None, PartyRole.RoleTypes.DIRECTOR) - print(role) + business.party_roles.append(role) business.save() - print(business.party_roles) - filing_relationship['entity']['identifier'] = str(role.id if relationship['valid_id'] else role.id + 1) - print(role.id) - print(filing_relationship['entity']['identifier']) + filing_relationship['entity']['identifier'] = str(role.party_id if relationship['valid_id'] else role.party_id + 1) transition['relationships'] = [filing_relationship]