From 2de8eed0253661b116c31c8d0180fc886545fb9b Mon Sep 17 00:00:00 2001 From: meawong Date: Wed, 14 Oct 2026 16:28:15 -0700 Subject: [PATCH 01/10] 35081 - Update filer + add bc req for records/registered offices --- .../validations/review_imported_data.py | 25 ++- .../validations/test_review_imported_data.py | 30 +++ queue_services/business-filer/poetry.lock | 12 +- queue_services/business-filer/pyproject.toml | 2 +- .../src/business_filer/common/filing.py | 3 +- .../filing_processors/review_imported_data.py | 110 +++++++++ .../src/business_filer/services/filer.py | 4 + .../test_review_imported_data.py | 208 ++++++++++++++++++ .../test_filer/test_review_imported_data.py | 137 ++++++++++++ 9 files changed, 522 insertions(+), 9 deletions(-) create mode 100644 queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py create mode 100644 queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py create mode 100644 queue_services/business-filer/tests/unit/test_filer/test_review_imported_data.py diff --git a/legal-api/src/legal_api/services/filings/validations/review_imported_data.py b/legal-api/src/legal_api/services/filings/validations/review_imported_data.py index abaa2f19ce..3b88e0085a 100644 --- a/legal-api/src/legal_api/services/filings/validations/review_imported_data.py +++ b/legal-api/src/legal_api/services/filings/validations/review_imported_data.py @@ -15,6 +15,7 @@ from datetime import UTC, datetime from http import HTTPStatus +import pycountry from flask_babel import _ as babel from business_model.models import Business, PartyRole @@ -39,15 +40,37 @@ def validate(business: Business, imported_data: dict) -> Error: msg = [] if filing_data.get("offices"): + offices = filing_data["offices"] msg.extend( validate_offices( imported_data, filing_type, allowed_types=["registeredOffice", "recordsOffice"], required_types=[], - bc_req=False, + bc_req=True, ) ) + for office_type, office in offices.items(): + if mailing_address := office.get("mailingAddress"): + mailing_address_path = ( + f"/filing/{filing_type}/offices/{office_type}/mailingAddress" + ) + if mailing_address.get("addressRegion") != "BC": + msg.append({ + "error": "Address Region must be 'BC'.", + "path": f"{mailing_address_path}/addressRegion", + }) + try: + country = pycountry.countries.search_fuzzy( + mailing_address["addressCountry"] + )[0].alpha_2 + if country != "CA": + raise LookupError + except LookupError: + msg.append({ + "error": "Address Country must be 'CA'.", + "path": f"{mailing_address_path}/addressCountry", + }) if filing_data.get("relationships"): msg.extend(validate_relationship_addresses(imported_data)) diff --git a/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py b/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py index bc12652e9e..fe20a56d73 100644 --- a/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py +++ b/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py @@ -153,6 +153,36 @@ def test_validate_offices_and_relationships(session): assert err is None +@pytest.mark.parametrize( + "office_type,address_type", + [ + ("registeredOffice", "deliveryAddress"), + ("registeredOffice", "mailingAddress"), + ("recordsOffice", "deliveryAddress"), + ("recordsOffice", "mailingAddress"), + ], +) +def test_validate_requires_bc_region_for_office_addresses(session, office_type, address_type): + """Require BC region for registered and records office mailing/delivery addresses.""" + business = _setup_business() + office = _valid_office() + office[address_type]["addressRegion"] = "AB" + filing = _build_filing({"offices": {office_type: office}}) + + err = validate(business, filing) + + if address_type == "deliveryAddress": + expected_path = f"/filing/reviewImportedData/offices/{office_type}" + else: + expected_path = f"/filing/reviewImportedData/offices/{office_type}/{address_type}/addressRegion" + assert err is not None + assert err.code == HTTPStatus.BAD_REQUEST + assert any( + item["error"] == "Address Region must be 'BC'." and item["path"] == expected_path + for item in err.msg + ) + + def test_validate_rejects_unknown_director_identifier(session): """Assert a relationship identifier that is not an active director is rejected.""" business = _setup_business() diff --git a/queue_services/business-filer/poetry.lock b/queue_services/business-filer/poetry.lock index fad74fa077..f220db5b34 100644 --- a/queue_services/business-filer/poetry.lock +++ b/queue_services/business-filer/poetry.lock @@ -294,7 +294,7 @@ files = [ [[package]] name = "business-model" -version = "3.4.18" +version = "3.4.19" description = "" optional = false python-versions = ">=3.13,<3.14" @@ -314,14 +314,14 @@ pg8000 = ">=1.31.2,<2.0.0" pycountry = ">=24.6.1,<25.0.0" pydantic = ">=2.10.6,<3.0.0" pytz = ">=2025.1,<2026.0" -registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.85"} +registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.86"} sql-versioning = {git = "https://github.com/bcgov/lear.git", rev = "main", subdirectory = "python/common/sql-versioning-alt"} [package.source] type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "a3b6442c2a3839ec222e974301dc983a431cfe76" +resolved_reference = "28d535527132941132ac0af0d1ec885d10a85390" subdirectory = "python/common/business-registry-model" [[package]] @@ -2814,7 +2814,7 @@ rpds-py = ">=0.7.0" [[package]] name = "registry_schemas" -version = "2.18.85" +version = "2.18.74" description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -2832,8 +2832,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.85" -resolved_reference = "d1e72d06bd71b3ae9ca92958f0cda4842de9cc97" +reference = "2.18.86" +resolved_reference = "7469def76cdb0314237bd613a111e180b9d6087d" [[package]] name = "requests" diff --git a/queue_services/business-filer/pyproject.toml b/queue_services/business-filer/pyproject.toml index 4c126b9bd4..7c85a3e011 100644 --- a/queue_services/business-filer/pyproject.toml +++ b/queue_services/business-filer/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "business-filer" -version = "3.0.40" +version = "3.0.41" description = "Business Registry Filer Service" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} diff --git a/queue_services/business-filer/src/business_filer/common/filing.py b/queue_services/business-filer/src/business_filer/common/filing.py index 10fb8e8118..b16d36bc4b 100644 --- a/queue_services/business-filer/src/business_filer/common/filing.py +++ b/queue_services/business-filer/src/business_filer/common/filing.py @@ -39,6 +39,7 @@ class FilingTypes(str, Enum): REGISTRATION = "registration" RESTORATION = "restoration" RESTORATIONAPPLICATION = "restorationApplication" + REVIEWIMPORTEDDATA = "reviewImportedData" SPECIALRESOLUTION = "specialResolution" TRANSITION = "transition" TRANSPARENCY_REGISTER = "transparencyRegister" @@ -50,4 +51,4 @@ class DissolutionTypes(str, Enum): COURT_ORDERED_LIQUIDATION = "courtOrderedLiquidation" INVOLUNTARY = "involuntary" VOLUNTARY = "voluntary" - VOLUNTARY_LIQUIDATION = "voluntaryLiquidation" \ No newline at end of file + VOLUNTARY_LIQUIDATION = "voluntaryLiquidation" diff --git a/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py b/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py new file mode 100644 index 0000000000..655385efe7 --- /dev/null +++ b/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py @@ -0,0 +1,110 @@ +# Copyright © 2026 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. +"""File processing rules and actions for review imported data filings.""" +from datetime import UTC, datetime + +from business_model.models import Business, Filing, PartyRole + +from business_filer.exceptions import QueueException +from business_filer.filing_meta import FilingMeta +from business_filer.filing_processors.filing_components import ( + create_address, + update_address, +) +from business_filer.filing_processors.filing_components.relationships import update_relationship_addresses + + +def _update_office(business: Business, office_type: str, office_info: dict) -> None: + """Update the supplied addresses for an existing office.""" + office = business.offices.filter_by(office_type=office_type).one_or_none() + if not office: + raise QueueException( + f"Could not find {office_type} office for business {business.id}." + ) + + for address_key, address_info in office_info.items(): + if not address_info: + continue + + address_type = address_key.replace("Address", "") + address = office.addresses.filter_by(address_type=address_type).one_or_none() + if address: + update_address(address, address_info) + else: + address = create_address(address_info, address_type) + office.addresses.append(address) + + +def process( + business: Business, + filing_rec: Filing, + filing_meta: FilingMeta, +) -> None: + """Apply supplied office and director address changes.""" + filing_json = filing_rec.filing_json["filing"]["reviewImportedData"] + + offices = filing_json.get("offices", {}) + changed_sections = [] + + relationships = filing_json.get("relationships", []) + if relationships: + active_directors = PartyRole.get_party_roles( + business.id, + datetime.now(UTC).date(), + PartyRole.RoleTypes.DIRECTOR.value, + ) + active_director_ids = {str(role.party_id) for role in active_directors} + + for relationship in relationships: + identifier = (relationship.get("entity") or {}).get("identifier") + if not identifier: + raise QueueException( + f"Could not resolve director identifier for business {business.id}." + ) + if str(identifier) not in active_director_ids: + raise QueueException( + f"Could not find active director with identifier {identifier} " + f"for business {business.id}." + ) + + for office_type, office_info in offices.items(): + _update_office(business, office_type, office_info) + if "offices" not in changed_sections: + changed_sections.append("offices") + + if relationships: + update_relationship_addresses(relationships, business) + changed_sections.append("directors") + + filing_meta.review_imported_data = {"sectionsChanged": changed_sections} diff --git a/queue_services/business-filer/src/business_filer/services/filer.py b/queue_services/business-filer/src/business_filer/services/filer.py index 2377bb2ddc..17da49fb4f 100644 --- a/queue_services/business-filer/src/business_filer/services/filer.py +++ b/queue_services/business-filer/src/business_filer/services/filer.py @@ -74,6 +74,7 @@ registrars_order, registration, restoration, + review_imported_data, special_resolution, transition, transparency_register, @@ -251,6 +252,9 @@ def process_filing(filing_message: FilingMessage): # noqa: PLR0915, PLR0912 case "restoration": restoration.process(business, filing, filing_submission, filing_meta) + case "reviewImportedData": + review_imported_data.process(business, filing_submission, filing_meta) + case "specialResolution": special_resolution.process(business, filing, filing_submission) diff --git a/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py b/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py new file mode 100644 index 0000000000..b0aa8980b4 --- /dev/null +++ b/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py @@ -0,0 +1,208 @@ +# Copyright © 2026 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. +"""Tests for Review Imported Data filing processing.""" +import copy +from datetime import UTC, datetime + +import pytest +from business_model.models import Address, Filing, Office, OfficeType, Party, PartyRole +from registry_schemas.example_data import FILING_TEMPLATE + +from business_filer.exceptions import QueueException +from business_filer.filing_meta import FilingMeta +from business_filer.filing_processors import review_imported_data +from business_filer.filing_processors.filing_components import create_address +from tests.unit import create_business + + +def _address(street: str) -> dict: + return { + "streetAddress": street, + "addressCity": "Victoria", + "addressCountry": "CA", + "addressRegion": "BC", + "postalCode": "V8V 1A1", + } + + +def _filing(payload: dict) -> Filing: + filing_json = copy.deepcopy(FILING_TEMPLATE) + filing_json["filing"]["header"]["name"] = "reviewImportedData" + filing_json["filing"]["reviewImportedData"] = payload + return Filing(filing_json=filing_json) + + +def _process(business, payload: dict) -> FilingMeta: + filing_meta = FilingMeta(application_date=datetime(2026, 1, 2, tzinfo=UTC)) + review_imported_data.process(business, _filing(payload), filing_meta) + return filing_meta + + +def _add_records_office(business) -> Office: + office = Office(office_type=OfficeType.RECORDS) + office.addresses.append(create_address(_address("Old records delivery"), Address.DELIVERY)) + office.addresses.append(create_address(_address("Old records mailing"), Address.MAILING)) + business.offices.append(office) + return office + + +def _add_director(business, session, cessation_date=None): + party = Party(first_name="ALEX", last_name="DIRECTOR") + party.delivery_address = create_address(_address("Old director delivery"), Address.DELIVERY) + party.mailing_address = create_address(_address("Old director mailing"), Address.MAILING) + party.save() + role = PartyRole( + role=PartyRole.RoleTypes.DIRECTOR.value, + appointment_date=datetime(2020, 1, 1, tzinfo=UTC), + cessation_date=cessation_date, + party_id=party.id, + business_id=business.id, + ) + role.save() + session.refresh(party) + return party, role + + +@pytest.fixture +def business(session): + return create_business("BC1234567", legal_type="BC", legal_name="Example Ltd.") + + +def test_process_offices_only(app, session, business): + """Update registered office addresses while leaving other business dates unchanged.""" + office = business.offices.filter_by(office_type=OfficeType.REGISTERED).one() + business.last_coa_date = datetime(2024, 1, 1, tzinfo=UTC) + + filing_meta = _process(business, { + "offices": { + "registeredOffice": { + "deliveryAddress": _address("New delivery"), + "mailingAddress": _address("New mailing"), + } + } + }) + + assert office.addresses.filter_by(address_type=Address.DELIVERY).one().street == "New delivery" + assert office.addresses.filter_by(address_type=Address.MAILING).one().street == "New mailing" + assert business.last_coa_date == datetime(2024, 1, 1, tzinfo=UTC) + assert filing_meta.asjson["reviewImportedData"]["sectionsChanged"] == ["offices"] + + +def test_process_records_office_only(app, session, business): + """Update only the records office when it is the only office in the payload.""" + records_office = _add_records_office(business) + registered_office = business.offices.filter_by(office_type=OfficeType.REGISTERED).one() + registered_delivery = registered_office.addresses.filter_by(address_type=Address.DELIVERY).one() + original_registered_street = registered_delivery.street + + filing_meta = _process(business, { + "offices": {"recordsOffice": { + "deliveryAddress": _address("New records delivery"), + "mailingAddress": _address("New records mailing"), + }} + }) + + assert records_office.addresses.filter_by(address_type=Address.DELIVERY).one().street == "New records delivery" + assert records_office.addresses.filter_by(address_type=Address.MAILING).one().street == "New records mailing" + assert registered_delivery.street == original_registered_street + assert filing_meta.asjson["reviewImportedData"]["sectionsChanged"] == ["offices"] + + +def test_process_directors_only_ignores_names_and_preserves_missing_address(app, session, business): + """Update one submitted director address without changing names or the omitted address.""" + party, role = _add_director(business, session) + original_appointment_date = role.appointment_date + + filing_meta = _process(business, { + "relationships": [{ + "entity": { + "identifier": str(party.id), + "givenName": "Changed", + "familyName": "Name", + }, + "deliveryAddress": _address("New director delivery"), + "mailingAddress": _address("New director mailing"), + "roles": [{"roleType": "Director", "appointmentDate": "2026-01-01"}], + }] + }) + + assert party.mailing_address.street == "New director mailing" + assert party.delivery_address.street == "New director delivery" + assert party.first_name == "ALEX" + assert party.last_name == "DIRECTOR" + assert role.appointment_date == original_appointment_date + assert role.cessation_date is None + assert business.last_cod_date is None + assert filing_meta.asjson["reviewImportedData"]["sectionsChanged"] == ["directors"] + + +def test_process_offices_and_directors(app, session, business): + """Update both filing sections and record both sections in metadata.""" + party, _ = _add_director(business, session) + office = business.offices.filter_by(office_type=OfficeType.REGISTERED).one() + + filing_meta = _process(business, { + "offices": {"registeredOffice": { + "deliveryAddress": _address("New office delivery"), + "mailingAddress": _address("New office mailing"), + }}, + "relationships": [{ + "entity": {"identifier": str(party.id)}, + "deliveryAddress": _address("New director delivery"), + }], + }) + + assert office.addresses.filter_by(address_type=Address.DELIVERY).one().street == "New office delivery" + assert party.delivery_address.street == "New director delivery" + assert filing_meta.asjson["reviewImportedData"]["sectionsChanged"] == ["offices", "directors"] + + +def test_process_partial_director_address_leaves_other_address_unchanged(app, session, business): + """Leave an omitted director address untouched when updating the supplied address.""" + party, _ = _add_director(business, session) + original_delivery = party.delivery_address.street + + _process(business, {"relationships": [{ + "entity": {"identifier": str(party.id)}, + "mailingAddress": _address("New director mailing"), + "roles": [{"roleType": "Director"}], + }]}) + + assert party.delivery_address.street == original_delivery + assert party.mailing_address.street == "New director mailing" + + +def test_process_empty_office_address_does_not_create_address(app, session, business): + """Ignore an empty address object rather than adding a blank address row.""" + office = business.offices.filter_by(office_type=OfficeType.REGISTERED).one() + + _process(business, {"offices": {"registeredOffice": {"mailingAddress": {}}}}) + + assert office.addresses.filter_by(address_type="mailing").count() == 0 + + +def test_process_unknown_director_identifier_raises(app, session, business): + """Reject an identifier that does not resolve to a party role.""" + with pytest.raises(QueueException) as exc_info: + _process(business, {"relationships": [{"entity": {"identifier": "999999"}}]}) + assert "Could not find active director" in exc_info.value.error + + +def test_process_ceased_director_identifier_raises(app, session, business): + """Reject a director whose role has been ceased.""" + party, _ = _add_director(business, session, cessation_date=datetime(2025, 1, 1, tzinfo=UTC)) + + with pytest.raises(QueueException) as exc_info: + _process(business, {"relationships": [{"entity": {"identifier": str(party.id)}}]}) + assert "Could not find active director" in exc_info.value.error diff --git a/queue_services/business-filer/tests/unit/test_filer/test_review_imported_data.py b/queue_services/business-filer/tests/unit/test_filer/test_review_imported_data.py new file mode 100644 index 0000000000..a954d90c67 --- /dev/null +++ b/queue_services/business-filer/tests/unit/test_filer/test_review_imported_data.py @@ -0,0 +1,137 @@ +# Copyright © 2026 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. +"""Filer dispatch tests for Review Imported Data.""" +import copy +from datetime import UTC, datetime + +from business_model.models import Business, Filing +from registry_schemas.example_data import FILING_TEMPLATE + +from business_filer.common.filing_message import FilingMessage +from business_filer.services.filer import process_filing +from tests.unit import create_business, create_filing, create_party, create_party_role + + +def test_process_filing_review_imported_data(app, session): + """Apply submitted office addresses and complete a Review Imported Data filing.""" + business = create_business("BC1234567", legal_type="BC", legal_name="Example Ltd.") + filing_json = copy.deepcopy(FILING_TEMPLATE) + filing_json["filing"]["header"]["name"] = "reviewImportedData" + filing_json["filing"]["business"]["identifier"] = business.identifier + filing_json["filing"]["reviewImportedData"] = { + "offices": { + "registeredOffice": { + "deliveryAddress": { + "streetAddress": "New delivery", + "addressCity": "Victoria", + "addressCountry": "CA", + "addressRegion": "BC", + "postalCode": "V8V 1A1", + }, + "mailingAddress": { + "streetAddress": "New mailing", + "addressCity": "Victoria", + "addressCountry": "CA", + "addressRegion": "BC", + "postalCode": "V8V 1A1", + }, + } + } + } + filing = create_filing("review-imported-data", filing_json, business.id) + + process_filing(FilingMessage(filing_identifier=filing.id)) + + completed_filing = Filing.find_by_id(filing.id) + updated_business = Business.find_by_internal_id(business.id) + registered_office = updated_business.offices.filter_by(office_type="registeredOffice").one() + + assert completed_filing.status == Filing.Status.COMPLETED.value + assert completed_filing.transaction_id + assert completed_filing.meta_data["reviewImportedData"]["sectionsChanged"] == ["offices"] + assert registered_office.addresses.filter_by(address_type="delivery").one().street == "New delivery" + assert registered_office.addresses.filter_by(address_type="mailing").one().street == "New mailing" + assert updated_business.last_modified == completed_filing.completion_date + + +def test_process_filing_review_imported_data_director_addresses(app, session): + """Update a director's addresses without changing their name.""" + business = create_business("BC1234568", legal_type="BC", legal_name="Example Ltd.") + party = create_party({ + "officer": {"firstName": "Phillip Tandy", "lastName": "Miller"}, + "deliveryAddress": { + "streetAddress": "delivery_address - address line one", + "addressCity": "delivery_address city", + "addressCountry": "CA", + "addressRegion": "BC", + "postalCode": "H0H0H0", + }, + "mailingAddress": { + "streetAddress": "mailing_address - address line one", + "addressCity": "mailing_address city", + "addressCountry": "CA", + "addressRegion": "BC", + "postalCode": "H0H0H0", + }, + }) + create_party_role(business, party, ["director"], datetime(2020, 1, 1, tzinfo=UTC)) + + filing_json = copy.deepcopy(FILING_TEMPLATE) + filing_json["filing"]["header"]["name"] = "reviewImportedData" + filing_json["filing"]["business"]["identifier"] = business.identifier + filing_json["filing"]["reviewImportedData"] = { + "relationships": [{ + "entity": { + "identifier": str(party.id), + "givenName": "Changed", + "middleInitial": "Tandy", + "familyName": "Miller", + "alternateName": "Phil Mills", + }, + "deliveryAddress": { + "streetAddress": "Changed Delivery", + "streetAddressAdditional": "", + "addressCity": "Vancouver", + "addressCountry": "CA", + "addressRegion": "BC", + "postalCode": "V0N4Y8", + "deliveryInstructions": "", + }, + "mailingAddress": { + "streetAddress": "Changed Mailing", + "streetAddressAdditional": "", + "addressCity": "Vancouver", + "addressCountry": "CA", + "addressRegion": "BC", + "postalCode": "V0N4Y8", + "deliveryInstructions": "", + }, + "roles": [{"roleType": "Director"}], + }] + } + filing = create_filing("review-imported-data-director", filing_json, business.id) + + process_filing(FilingMessage(filing_identifier=filing.id)) + + completed_filing = Filing.find_by_id(filing.id) + updated_business = Business.find_by_internal_id(business.id) + director_role = updated_business.party_roles.filter_by(party_id=party.id, role="director").one() + + assert completed_filing.status == Filing.Status.COMPLETED.value + assert completed_filing.transaction_id + assert completed_filing.meta_data["reviewImportedData"]["sectionsChanged"] == ["directors"] + assert director_role.party.first_name == "PHILLIP TANDY" + assert director_role.party.last_name == "MILLER" + assert director_role.party.delivery_address.street == "Changed Delivery" + assert director_role.party.mailing_address.street == "Changed Mailing" From 0866430c566323d65ba1f5d6a2fd793eeafe50cb Mon Sep 17 00:00:00 2001 From: meawong Date: Wed, 14 Oct 2026 16:52:54 -0700 Subject: [PATCH 02/10] 35081 - cleanup --- queue_services/business-filer/poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queue_services/business-filer/poetry.lock b/queue_services/business-filer/poetry.lock index f220db5b34..e1a3d35e2d 100644 --- a/queue_services/business-filer/poetry.lock +++ b/queue_services/business-filer/poetry.lock @@ -2814,7 +2814,7 @@ rpds-py = ">=0.7.0" [[package]] name = "registry_schemas" -version = "2.18.74" +version = "2.18.86" description = "A short description of the project" optional = false python-versions = ">=3.6" From e4a93e25400092c831b3ae85db36c5ac516d895b Mon Sep 17 00:00:00 2001 From: meawong Date: Wed, 14 Oct 2026 17:27:06 -0700 Subject: [PATCH 03/10] 35081 - Update unit tests --- .../test_review_imported_data.py | 29 +++++++------------ .../test_filer/test_review_imported_data.py | 2 +- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py b/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py index b0aa8980b4..70c7a602e2 100644 --- a/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py +++ b/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py @@ -29,10 +29,10 @@ def _address(street: str) -> dict: return { "streetAddress": street, - "addressCity": "Victoria", + "addressCity": "delivery_address city", "addressCountry": "CA", "addressRegion": "BC", - "postalCode": "V8V 1A1", + "postalCode": "H0H0H0", } @@ -58,7 +58,7 @@ def _add_records_office(business) -> Office: def _add_director(business, session, cessation_date=None): - party = Party(first_name="ALEX", last_name="DIRECTOR") + party = Party(first_name="Phillip Tandy", last_name="Miller") party.delivery_address = create_address(_address("Old director delivery"), Address.DELIVERY) party.mailing_address = create_address(_address("Old director mailing"), Address.MAILING) party.save() @@ -107,10 +107,12 @@ def test_process_records_office_only(app, session, business): original_registered_street = registered_delivery.street filing_meta = _process(business, { - "offices": {"recordsOffice": { - "deliveryAddress": _address("New records delivery"), - "mailingAddress": _address("New records mailing"), - }} + "offices": { + "recordsOffice": { + "deliveryAddress": _address("New records delivery"), + "mailingAddress": _address("New records mailing"), + } + } }) assert records_office.addresses.filter_by(address_type=Address.DELIVERY).one().street == "New records delivery" @@ -139,8 +141,8 @@ def test_process_directors_only_ignores_names_and_preserves_missing_address(app, assert party.mailing_address.street == "New director mailing" assert party.delivery_address.street == "New director delivery" - assert party.first_name == "ALEX" - assert party.last_name == "DIRECTOR" + assert party.first_name == "Phillip Tandy" + assert party.last_name == "Miller" assert role.appointment_date == original_appointment_date assert role.cessation_date is None assert business.last_cod_date is None @@ -183,15 +185,6 @@ def test_process_partial_director_address_leaves_other_address_unchanged(app, se assert party.mailing_address.street == "New director mailing" -def test_process_empty_office_address_does_not_create_address(app, session, business): - """Ignore an empty address object rather than adding a blank address row.""" - office = business.offices.filter_by(office_type=OfficeType.REGISTERED).one() - - _process(business, {"offices": {"registeredOffice": {"mailingAddress": {}}}}) - - assert office.addresses.filter_by(address_type="mailing").count() == 0 - - def test_process_unknown_director_identifier_raises(app, session, business): """Reject an identifier that does not resolve to a party role.""" with pytest.raises(QueueException) as exc_info: diff --git a/queue_services/business-filer/tests/unit/test_filer/test_review_imported_data.py b/queue_services/business-filer/tests/unit/test_filer/test_review_imported_data.py index a954d90c67..8ce8477abc 100644 --- a/queue_services/business-filer/tests/unit/test_filer/test_review_imported_data.py +++ b/queue_services/business-filer/tests/unit/test_filer/test_review_imported_data.py @@ -23,7 +23,7 @@ from tests.unit import create_business, create_filing, create_party, create_party_role -def test_process_filing_review_imported_data(app, session): +def test_process_filing_review_imported_data_office_addresses(app, session): """Apply submitted office addresses and complete a Review Imported Data filing.""" business = create_business("BC1234567", legal_type="BC", legal_name="Example Ltd.") filing_json = copy.deepcopy(FILING_TEMPLATE) From f161b823993d57800434b1e5c515b3b2bf474b49 Mon Sep 17 00:00:00 2001 From: meawong Date: Wed, 14 Oct 2026 17:44:50 -0700 Subject: [PATCH 04/10] 35081 - Remove redundant check covered by api validation --- .../filing_processors/review_imported_data.py | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py b/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py index 655385efe7..fe65d94990 100644 --- a/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py +++ b/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py @@ -42,7 +42,10 @@ create_address, update_address, ) -from business_filer.filing_processors.filing_components.relationships import update_relationship_addresses +from business_filer.filing_processors.filing_components.relationships import ( + _str_to_int, + update_relationship_addresses, +) def _update_office(business: Business, office_type: str, office_info: dict) -> None: @@ -66,6 +69,29 @@ def _update_office(business: Business, office_type: str, office_info: dict) -> N office.addresses.append(address) +def _check_active_directors(business: Business, relationships: list[dict]) -> None: + """Ensure every relationship still references an active director. + + Validation already checks this at submission, but a director may have been + ceased between submission and processing, so this guards that gap. + """ + active_director_ids = { + role.party_id + for role in PartyRole.get_party_roles( + business.id, + datetime.now(UTC).date(), + PartyRole.RoleTypes.DIRECTOR.value, + ) + } + for relationship in relationships: + identifier = relationship["entity"]["identifier"] + if _str_to_int(identifier) not in active_director_ids: + raise QueueException( + f"Could not find active director with identifier {identifier} " + f"for business {business.id}." + ) + + def process( business: Business, filing_rec: Filing, @@ -73,35 +99,17 @@ def process( ) -> None: """Apply supplied office and director address changes.""" filing_json = filing_rec.filing_json["filing"]["reviewImportedData"] - offices = filing_json.get("offices", {}) + relationships = filing_json.get("relationships", []) changed_sections = [] - relationships = filing_json.get("relationships", []) if relationships: - active_directors = PartyRole.get_party_roles( - business.id, - datetime.now(UTC).date(), - PartyRole.RoleTypes.DIRECTOR.value, - ) - active_director_ids = {str(role.party_id) for role in active_directors} - - for relationship in relationships: - identifier = (relationship.get("entity") or {}).get("identifier") - if not identifier: - raise QueueException( - f"Could not resolve director identifier for business {business.id}." - ) - if str(identifier) not in active_director_ids: - raise QueueException( - f"Could not find active director with identifier {identifier} " - f"for business {business.id}." - ) + _check_active_directors(business, relationships) for office_type, office_info in offices.items(): _update_office(business, office_type, office_info) - if "offices" not in changed_sections: - changed_sections.append("offices") + if offices: + changed_sections.append("offices") if relationships: update_relationship_addresses(relationships, business) From bcd0eed148d844efd2b804ccfd77caab6bdaff73 Mon Sep 17 00:00:00 2001 From: meawong Date: Wed, 14 Oct 2026 19:21:29 -0700 Subject: [PATCH 05/10] 35081 - Fix sonar --- .../validations/review_imported_data.py | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/legal-api/src/legal_api/services/filings/validations/review_imported_data.py b/legal-api/src/legal_api/services/filings/validations/review_imported_data.py index 3b88e0085a..01d0253392 100644 --- a/legal-api/src/legal_api/services/filings/validations/review_imported_data.py +++ b/legal-api/src/legal_api/services/filings/validations/review_imported_data.py @@ -40,7 +40,6 @@ def validate(business: Business, imported_data: dict) -> Error: msg = [] if filing_data.get("offices"): - offices = filing_data["offices"] msg.extend( validate_offices( imported_data, @@ -50,27 +49,8 @@ def validate(business: Business, imported_data: dict) -> Error: bc_req=True, ) ) - for office_type, office in offices.items(): - if mailing_address := office.get("mailingAddress"): - mailing_address_path = ( - f"/filing/{filing_type}/offices/{office_type}/mailingAddress" - ) - if mailing_address.get("addressRegion") != "BC": - msg.append({ - "error": "Address Region must be 'BC'.", - "path": f"{mailing_address_path}/addressRegion", - }) - try: - country = pycountry.countries.search_fuzzy( - mailing_address["addressCountry"] - )[0].alpha_2 - if country != "CA": - raise LookupError - except LookupError: - msg.append({ - "error": "Address Country must be 'CA'.", - "path": f"{mailing_address_path}/addressCountry", - }) + + msg.extend(validate_office_mailing_addresses(filing_data["offices"], filing_type)) if filing_data.get("relationships"): msg.extend(validate_relationship_addresses(imported_data)) @@ -82,6 +62,40 @@ def validate(business: Business, imported_data: dict) -> Error: return None +def validate_office_mailing_addresses(offices: dict, filing_type: str) -> list: + """Validate mailing address region and country requirements. + + The shared validators only enforce the BC requirement for delivery + addresses, so explicitly validate the mailing address region and + country here. + """ + msg = [] + + for office_type, office in offices.items(): + if mailing_address := office.get("mailingAddress"): + mailing_address_path = ( + f"/filing/{filing_type}/offices/{office_type}/mailingAddress" + ) + if mailing_address.get("addressRegion") != "BC": + msg.append({ + "error": "Address Region must be 'BC'.", + "path": f"{mailing_address_path}/addressRegion", + }) + try: + country = pycountry.countries.search_fuzzy( + mailing_address["addressCountry"] + )[0].alpha_2 + if country != "CA": + raise LookupError + except LookupError: + msg.append({ + "error": "Address Country must be 'CA'.", + "path": f"{mailing_address_path}/addressCountry", + }) + + return msg + + def validate_relationship_addresses(imported_data: dict) -> list: """Validate director relationship addresses.""" filing_type = "reviewImportedData" From 24c839bb5cc1cc9c53c018b56b15b029f68c0e04 Mon Sep 17 00:00:00 2001 From: meawong Date: Thu, 15 Oct 2026 14:15:08 -0700 Subject: [PATCH 06/10] 35081 - PR review updates --- legal-api/poetry.lock | 4 +-- queue_services/business-filer/poetry.lock | 4 +-- .../filing_processors/review_imported_data.py | 29 ++----------------- .../test_review_imported_data.py | 11 ++----- 4 files changed, 9 insertions(+), 39 deletions(-) diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index c7f825efa3..1abeaa77f1 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -316,7 +316,7 @@ files = [ [[package]] name = "business-model" -version = "3.4.19" +version = "3.4.20" description = "" optional = false python-versions = ">=3.13,<3.14" @@ -343,7 +343,7 @@ sql-versioning = {git = "https://github.com/bcgov/lear.git", rev = "main", subdi type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "a98447127ac49ffcd9b9d4f78a28b637f5cf6cf3" +resolved_reference = "e8794632035a138885f01c9f698e7ac38ca398be" subdirectory = "python/common/business-registry-model" [[package]] diff --git a/queue_services/business-filer/poetry.lock b/queue_services/business-filer/poetry.lock index e1a3d35e2d..99b68a0c77 100644 --- a/queue_services/business-filer/poetry.lock +++ b/queue_services/business-filer/poetry.lock @@ -294,7 +294,7 @@ files = [ [[package]] name = "business-model" -version = "3.4.19" +version = "3.4.20" description = "" optional = false python-versions = ">=3.13,<3.14" @@ -321,7 +321,7 @@ sql-versioning = {git = "https://github.com/bcgov/lear.git", rev = "main", subdi type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "28d535527132941132ac0af0d1ec885d10a85390" +resolved_reference = "e8794632035a138885f01c9f698e7ac38ca398be" subdirectory = "python/common/business-registry-model" [[package]] diff --git a/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py b/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py index fe65d94990..58189615cc 100644 --- a/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py +++ b/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py @@ -38,9 +38,8 @@ from business_filer.exceptions import QueueException from business_filer.filing_meta import FilingMeta -from business_filer.filing_processors.filing_components import ( - create_address, - update_address, +from business_filer.filing_processors.filing_components.offices import ( + update_or_create_offices, ) from business_filer.filing_processors.filing_components.relationships import ( _str_to_int, @@ -48,27 +47,6 @@ ) -def _update_office(business: Business, office_type: str, office_info: dict) -> None: - """Update the supplied addresses for an existing office.""" - office = business.offices.filter_by(office_type=office_type).one_or_none() - if not office: - raise QueueException( - f"Could not find {office_type} office for business {business.id}." - ) - - for address_key, address_info in office_info.items(): - if not address_info: - continue - - address_type = address_key.replace("Address", "") - address = office.addresses.filter_by(address_type=address_type).one_or_none() - if address: - update_address(address, address_info) - else: - address = create_address(address_info, address_type) - office.addresses.append(address) - - def _check_active_directors(business: Business, relationships: list[dict]) -> None: """Ensure every relationship still references an active director. @@ -106,9 +84,8 @@ def process( if relationships: _check_active_directors(business, relationships) - for office_type, office_info in offices.items(): - _update_office(business, office_type, office_info) if offices: + update_or_create_offices(business, offices) changed_sections.append("offices") if relationships: diff --git a/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py b/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py index 70c7a602e2..9817805244 100644 --- a/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py +++ b/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py @@ -49,13 +49,6 @@ def _process(business, payload: dict) -> FilingMeta: return filing_meta -def _add_records_office(business) -> Office: - office = Office(office_type=OfficeType.RECORDS) - office.addresses.append(create_address(_address("Old records delivery"), Address.DELIVERY)) - office.addresses.append(create_address(_address("Old records mailing"), Address.MAILING)) - business.offices.append(office) - return office - def _add_director(business, session, cessation_date=None): party = Party(first_name="Phillip Tandy", last_name="Miller") @@ -100,8 +93,7 @@ def test_process_offices_only(app, session, business): def test_process_records_office_only(app, session, business): - """Update only the records office when it is the only office in the payload.""" - records_office = _add_records_office(business) + """Create a missing records office without changing the registered office.""" registered_office = business.offices.filter_by(office_type=OfficeType.REGISTERED).one() registered_delivery = registered_office.addresses.filter_by(address_type=Address.DELIVERY).one() original_registered_street = registered_delivery.street @@ -115,6 +107,7 @@ def test_process_records_office_only(app, session, business): } }) + records_office = business.offices.filter_by(office_type=OfficeType.RECORDS).one() assert records_office.addresses.filter_by(address_type=Address.DELIVERY).one().street == "New records delivery" assert records_office.addresses.filter_by(address_type=Address.MAILING).one().street == "New records mailing" assert registered_delivery.street == original_registered_street From 2c5c1d5304eed0f2c5b21f16fb3c9f97d7bf3c64 Mon Sep 17 00:00:00 2001 From: meawong Date: Thu, 15 Oct 2026 16:19:20 -0700 Subject: [PATCH 07/10] 35081 - Add ff gate --- legal-api/src/legal_api/services/authz.py | 12 +++- .../filings/validations/validation.py | 6 +- .../tests/unit/resources/v2/test_business.py | 2 +- .../validations/test_review_imported_data.py | 17 ++++++ .../tests/unit/services/test_authorization.py | 59 ++++++++++++++----- 5 files changed, 77 insertions(+), 19 deletions(-) diff --git a/legal-api/src/legal_api/services/authz.py b/legal-api/src/legal_api/services/authz.py index 197ffc7990..7e539bc573 100644 --- a/legal-api/src/legal_api/services/authz.py +++ b/legal-api/src/legal_api/services/authz.py @@ -166,7 +166,7 @@ def has_any_roles(jwt: JwtManager, roles: list[str]) -> bool: return any(bool(jwt.validate_roles(request_ctx.current_user, [role])) for role in roles) -def get_allowable_filings_dict(is_authorization: bool = False): +def get_allowable_filings_dict(is_authorization: bool = False): # noqa: PLR0912 """Return dictionary containing rules for when filings are allowed.""" # importing here to avoid circular dependencies # pylint: disable=import-outside-toplevel @@ -708,6 +708,16 @@ def get_allowable_filings_dict(is_authorization: bool = False): else: del allowable_filings_dict[user_type][Business.State.ACTIVE.value][filing_type] + enabled_features = flags.value( + "enable-new-feature", + request_context.user, + request_context.account_id, + ) or [] + if "reviewImportedData" not in enabled_features: + for user_type_dict in allowable_filings_dict.values(): + for business_state_dict in user_type_dict.values(): + business_state_dict.pop("reviewImportedData", None) + # Remove some alteration blockers when authorization is not required if not is_authorization: for user_type_dict in allowable_filings_dict.values(): 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 5993d7a6a3..f5f162cee2 100644 --- a/legal-api/src/legal_api/services/filings/validations/validation.py +++ b/legal-api/src/legal_api/services/filings/validations/validation.py @@ -203,7 +203,11 @@ def validate(business: Business, # noqa: PLR0915, PLR0912, PLR0911 err = registration_validate(filing_json) elif k == Filing.FILINGS["reviewImportedData"].get("name"): - err = review_imported_data_validate(business,filing_json) + enabled_features = flags.value("enable-new-feature", account_id=account_id) or [] + if "reviewImportedData" not in enabled_features: + err = Error(HTTPStatus.FORBIDDEN, [{"error": "Review Imported Data filing is not enabled."}]) + else: + err = review_imported_data_validate(business, filing_json) elif k == Filing.FILINGS["changeOfRegistration"].get("name"): err = change_of_registration_validate(business, filing_json) diff --git a/legal-api/tests/unit/resources/v2/test_business.py b/legal-api/tests/unit/resources/v2/test_business.py index 9cd94152e3..7e2a1c8282 100644 --- a/legal-api/tests/unit/resources/v2/test_business.py +++ b/legal-api/tests/unit/resources/v2/test_business.py @@ -1036,7 +1036,7 @@ def test_get_could_file(session, client, jwt, monkeypatch): monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', diff --git a/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py b/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py index fe20a56d73..922c54983e 100644 --- a/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py +++ b/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py @@ -119,6 +119,23 @@ def test_validate_offices_only(session): assert err is None +def test_validate_rejects_review_imported_data_when_feature_is_disabled(session, monkeypatch): + """Reject the filing when reviewImportedData is absent from the feature flag.""" + business = _setup_business() + filing = _build_filing({ + "offices": { + "registeredOffice": _valid_office(), + }, + }) + monkeypatch.setattr("legal_api.services.flags.value", lambda *args, **kwargs: []) + + err = validate(business, filing) + + assert err is not None + assert err.code == HTTPStatus.FORBIDDEN + assert err.msg == [{"error": "Review Imported Data filing is not enabled."}] + + def test_validate_relationships_only(session): """Assert a filing containing relationships only passes validation.""" business = _setup_business() diff --git a/legal-api/tests/unit/services/test_authorization.py b/legal-api/tests/unit/services/test_authorization.py index 9a8a74b20a..3a5c197b74 100644 --- a/legal-api/tests/unit/services/test_authorization.py +++ b/legal-api/tests/unit/services/test_authorization.py @@ -32,7 +32,7 @@ from legal_api import create_app from legal_api.services.authz import BASIC_USER, COLIN_SVC_ROLE, CONTACT_CENTRE_STAFF_ROLE , MAXIMUS_STAFF_ROLE, \ PUBLIC_USER, STAFF_ROLE, SBC_STAFF_ROLE, \ - authorized, is_allowed, get_allowed, get_allowed_filings, get_allowable_actions + authorized, is_allowed, get_allowed, get_allowed_filings, get_allowable_actions, get_allowable_filings_dict from legal_api.services.permissions import PermissionService from legal_api.services.warnings.business.business_checks import WarningType from registry_schemas.example_data import ( @@ -678,7 +678,7 @@ def test_get_allowed(monkeypatch, app, jwt, test_name, state, legal_types, usern monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) for legal_type in legal_types: filing_types = get_allowed(state, legal_type, jwt) @@ -1016,7 +1016,7 @@ def test_is_allowed(monkeypatch, app, session, jwt, test_name, state, filing_typ monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) for legal_type in legal_types: business = create_business(legal_type, state) @@ -1216,7 +1216,7 @@ def test_get_allowed_actions(monkeypatch, app, session, jwt, requests_mock, monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'legal_api.services.flags.is_on', @@ -1545,7 +1545,7 @@ def test_get_allowed_filings(monkeypatch, app, session, jwt, test_name, business monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', @@ -1888,7 +1888,7 @@ def test_get_allowed_filings_blocker_not_in_good_standing(monkeypatch, app, sess monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', @@ -2410,7 +2410,7 @@ def test_allowed_filings_warnings(monkeypatch, app, session, jwt, test_name, sta monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', @@ -2429,7 +2429,8 @@ def test_allowed_filings_corps_missing_info_warnings(monkeypatch, app, session, """Assert corps with MISSING_REQUIRED_BUSINESS_INFO warnings can still file reviewImportedData but not correction.""" with jwt_request_context(app, jwt, roles=roles, username=username): monkeypatch.setattr('legal_api.services.flags.value', - lambda flag, _user, _account_id: '' if flag == 'enabled-specific-filings' else {}) + lambda flag, _user, _account_id: '' if flag == 'enabled-specific-filings' + else ['reviewImportedData'] if flag == 'enable-new-feature' else {}) monkeypatch.setattr('business_model.models.User.get_or_create_user_by_jwt', lambda _: None) for legal_type in ['BC', 'BEN', 'CC', 'ULC', 'C', 'CBEN', 'CCC', 'CUL']: business = create_business(legal_type, Business.State.ACTIVE) @@ -2789,7 +2790,7 @@ def test_allowed_filings_state_filing_check(monkeypatch, app, session, jwt, test monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', @@ -3033,12 +3034,12 @@ def test_allowed_filings_completed_filing_check(monkeypatch, app, session, jwt, monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', @@ -3216,7 +3217,7 @@ def test_get_allowed_filings_blocker_in_dissolution(monkeypatch, app, session, j monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', @@ -3419,7 +3420,7 @@ def test_allowed_filings_notice_of_withdrawal(monkeypatch, app, session, jwt, te monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', @@ -3515,7 +3516,7 @@ def test_allowed_filings_specific_disabled(monkeypatch, app, session, jwt, monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: enabled_filings - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', @@ -3530,6 +3531,32 @@ def test_allowed_filings_specific_disabled(monkeypatch, app, session, jwt, assert allowed_filing_types == expected +@pytest.mark.parametrize( + 'enabled_features, expected', + [ + (['reviewImportedData'], True), + ([], False), + ], +) +def test_allowed_filings_review_imported_data_feature_flag(monkeypatch, app, jwt, enabled_features, expected): + """Assert that Review Imported Data is exposed only when its feature flag is enabled.""" + with jwt_request_context(app, jwt, roles=[STAFF_ROLE], username='staff'): + monkeypatch.setattr( + 'legal_api.services.flags.value', + lambda flag, _user, _account_id: enabled_features if flag == 'enable-new-feature' else '', + ) + allowable_filings = get_allowable_filings_dict() + + assert ('reviewImportedData' in allowable_filings['staff'][Business.State.ACTIVE]) is expected + assert ('reviewImportedData' in allowable_filings['general'][Business.State.ACTIVE]) is expected + if not expected: + assert all( + 'reviewImportedData' not in business_state_dict + for user_type_dict in allowable_filings.values() + for business_state_dict in user_type_dict.values() + ) + + @pytest.mark.parametrize( 'test_name,username,roles,dods,expected', [ @@ -3658,7 +3685,7 @@ def test_get_allowed_filings_blocker_max_delays(monkeypatch, app, session, jwt, monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', @@ -3795,7 +3822,7 @@ def test_get_allowed_filings_in_liquidation(monkeypatch, app, session, jwt, test monkeypatch.setattr( 'legal_api.services.flags.value', lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" - if flag == 'enabled-specific-filings' else {} + if flag == 'enabled-specific-filings' else ['reviewImportedData'] if flag == 'enable-new-feature' else {} ) monkeypatch.setattr( 'business_model.models.User.get_or_create_user_by_jwt', From 8f141e3812b87b276731cf6871ed9abf76715d03 Mon Sep 17 00:00:00 2001 From: meawong Date: Tue, 20 Oct 2026 08:30:31 -0700 Subject: [PATCH 08/10] 35081 - Bump version after rebase --- 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 c1abf26d1e..46da0b9ce2 100644 --- a/legal-api/pyproject.toml +++ b/legal-api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "legal-api" -version = "3.1.46" +version = "3.1.47" description = "" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} From aea73ddcf38e9cd13144079b908c99d0c9e55b24 Mon Sep 17 00:00:00 2001 From: meawong Date: Mon, 19 Oct 2026 09:05:38 -0700 Subject: [PATCH 09/10] 35081 - Remove mailing address requirement --- .../validations/review_imported_data.py | 36 ------------------- .../validations/test_review_imported_data.py | 20 +++++------ 2 files changed, 10 insertions(+), 46 deletions(-) diff --git a/legal-api/src/legal_api/services/filings/validations/review_imported_data.py b/legal-api/src/legal_api/services/filings/validations/review_imported_data.py index 01d0253392..1d48a9bc44 100644 --- a/legal-api/src/legal_api/services/filings/validations/review_imported_data.py +++ b/legal-api/src/legal_api/services/filings/validations/review_imported_data.py @@ -50,8 +50,6 @@ def validate(business: Business, imported_data: dict) -> Error: ) ) - msg.extend(validate_office_mailing_addresses(filing_data["offices"], filing_type)) - if filing_data.get("relationships"): msg.extend(validate_relationship_addresses(imported_data)) msg.extend(validate_active_directors(business, imported_data)) @@ -62,40 +60,6 @@ def validate(business: Business, imported_data: dict) -> Error: return None -def validate_office_mailing_addresses(offices: dict, filing_type: str) -> list: - """Validate mailing address region and country requirements. - - The shared validators only enforce the BC requirement for delivery - addresses, so explicitly validate the mailing address region and - country here. - """ - msg = [] - - for office_type, office in offices.items(): - if mailing_address := office.get("mailingAddress"): - mailing_address_path = ( - f"/filing/{filing_type}/offices/{office_type}/mailingAddress" - ) - if mailing_address.get("addressRegion") != "BC": - msg.append({ - "error": "Address Region must be 'BC'.", - "path": f"{mailing_address_path}/addressRegion", - }) - try: - country = pycountry.countries.search_fuzzy( - mailing_address["addressCountry"] - )[0].alpha_2 - if country != "CA": - raise LookupError - except LookupError: - msg.append({ - "error": "Address Country must be 'CA'.", - "path": f"{mailing_address_path}/addressCountry", - }) - - return msg - - def validate_relationship_addresses(imported_data: dict) -> list: """Validate director relationship addresses.""" filing_type = "reviewImportedData" diff --git a/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py b/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py index 922c54983e..55d6ce5bad 100644 --- a/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py +++ b/legal-api/tests/unit/services/filings/validations/test_review_imported_data.py @@ -180,7 +180,7 @@ def test_validate_offices_and_relationships(session): ], ) def test_validate_requires_bc_region_for_office_addresses(session, office_type, address_type): - """Require BC region for registered and records office mailing/delivery addresses.""" + """Require BC region for delivery addresses, but allow other regions for mailing addresses.""" business = _setup_business() office = _valid_office() office[address_type]["addressRegion"] = "AB" @@ -188,16 +188,16 @@ def test_validate_requires_bc_region_for_office_addresses(session, office_type, err = validate(business, filing) - if address_type == "deliveryAddress": - expected_path = f"/filing/reviewImportedData/offices/{office_type}" + if address_type == "mailingAddress": + assert err is None else: - expected_path = f"/filing/reviewImportedData/offices/{office_type}/{address_type}/addressRegion" - assert err is not None - assert err.code == HTTPStatus.BAD_REQUEST - assert any( - item["error"] == "Address Region must be 'BC'." and item["path"] == expected_path - for item in err.msg - ) + expected_path = f"/filing/reviewImportedData/offices/{office_type}" + assert err is not None + assert err.code == HTTPStatus.BAD_REQUEST + assert any( + item["error"] == "Address Region must be 'BC'." and item["path"] == expected_path + for item in err.msg + ) def test_validate_rejects_unknown_director_identifier(session): From 75e6fad00f483b077461476bcd8ffb2ee4bfaeaf Mon Sep 17 00:00:00 2001 From: meawong Date: Mon, 19 Oct 2026 09:06:17 -0700 Subject: [PATCH 10/10] no message --- .../services/filings/validations/review_imported_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/legal-api/src/legal_api/services/filings/validations/review_imported_data.py b/legal-api/src/legal_api/services/filings/validations/review_imported_data.py index 1d48a9bc44..3a6ff28c28 100644 --- a/legal-api/src/legal_api/services/filings/validations/review_imported_data.py +++ b/legal-api/src/legal_api/services/filings/validations/review_imported_data.py @@ -15,7 +15,6 @@ from datetime import UTC, datetime from http import HTTPStatus -import pycountry from flask_babel import _ as babel from business_model.models import Business, PartyRole