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/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"} 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/review_imported_data.py b/legal-api/src/legal_api/services/filings/validations/review_imported_data.py index abaa2f19ce..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 @@ -45,7 +45,7 @@ def validate(business: Business, imported_data: dict) -> Error: filing_type, allowed_types=["registeredOffice", "recordsOffice"], required_types=[], - bc_req=False, + bc_req=True, ) ) 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 bc12652e9e..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 @@ -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() @@ -153,6 +170,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 delivery addresses, but allow other regions for mailing 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 == "mailingAddress": + assert err is None + else: + 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): """Assert a relationship identifier that is not an active director is rejected.""" 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', diff --git a/queue_services/business-filer/poetry.lock b/queue_services/business-filer/poetry.lock index fad74fa077..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.18" +version = "3.4.20" 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 = "e8794632035a138885f01c9f698e7ac38ca398be" 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.86" 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..58189615cc --- /dev/null +++ b/queue_services/business-filer/src/business_filer/filing_processors/review_imported_data.py @@ -0,0 +1,95 @@ +# 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.offices import ( + update_or_create_offices, +) +from business_filer.filing_processors.filing_components.relationships import ( + _str_to_int, + update_relationship_addresses, +) + + +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, + filing_meta: FilingMeta, +) -> 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 = [] + + if relationships: + _check_active_directors(business, relationships) + + if offices: + update_or_create_offices(business, offices) + 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..9817805244 --- /dev/null +++ b/queue_services/business-filer/tests/unit/filing_processors/test_review_imported_data.py @@ -0,0 +1,194 @@ +# 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": "delivery_address city", + "addressCountry": "CA", + "addressRegion": "BC", + "postalCode": "H0H0H0", + } + + +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_director(business, session, cessation_date=None): + 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() + 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): + """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 + + filing_meta = _process(business, { + "offices": { + "recordsOffice": { + "deliveryAddress": _address("New records delivery"), + "mailingAddress": _address("New records mailing"), + } + } + }) + + 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 + 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 == "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 + 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_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..8ce8477abc --- /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_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) + 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"