diff --git a/queue_services/business-filer/poetry.lock b/queue_services/business-filer/poetry.lock index f7fc95e170..8cf9962a31 100644 --- a/queue_services/business-filer/poetry.lock +++ b/queue_services/business-filer/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "alembic" @@ -113,7 +113,7 @@ files = [ [[package]] name = "business-model" -version = "3.3.22" +version = "3.3.23" description = "" optional = false python-versions = ">=3.13,<3.14" @@ -133,14 +133,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.62"} +registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.64"} 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 = "6ad7e1989f612f541c3468d459ccfff784bb6a29" +resolved_reference = "cd7ebab1c899da77ed6f1977dac23375094e5f30" subdirectory = "python/common/business-registry-model" [[package]] @@ -867,7 +867,7 @@ files = [ [package.dependencies] google-auth = ">=2.14.1,<3.0.0" googleapis-common-protos = ">=1.56.2,<2.0.0" -grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""} protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -875,7 +875,7 @@ requests = ">=2.18.0,<3.0.0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] +grpc = ["grpcio (>=1.33.2,<2.0.dev0)", "grpcio (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -917,11 +917,11 @@ files = [ ] [package.dependencies] -google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" -google-auth = ">=1.25.0,<3.0dev" +google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0.dev0" +google-auth = ">=1.25.0,<3.0.dev0" [package.extras] -grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] +grpc = ["grpcio (>=1.38.0,<2.0.dev0)", "grpcio-status (>=1.38.0,<2.0.dev0)"] [[package]] name = "google-cloud-datastore" @@ -936,11 +936,11 @@ files = [ ] [package.dependencies] -google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" -google-cloud-core = ">=1.4.0,<3.0.0dev" -proto-plus = {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""} -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" +google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0.dev0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +google-cloud-core = ">=1.4.0,<3.0.0.dev0" +proto-plus = {version = ">=1.22.2,<2.0.0.dev0", markers = "python_version >= \"3.11\""} +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" [package.extras] libcst = ["libcst (>=0.2.5)"] @@ -1174,7 +1174,7 @@ files = [ [package.dependencies] googleapis-common-protos = ">=1.5.5" grpcio = ">=1.71.0" -protobuf = ">=5.26.1,<6.0dev" +protobuf = ">=5.26.1,<6.0.dev0" [[package]] name = "gunicorn" @@ -1358,7 +1358,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format\""} idna = {version = "*", optional = true, markers = "extra == \"format\""} isoduration = {version = "*", optional = true, markers = "extra == \"format\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} @@ -2113,7 +2113,7 @@ rpds-py = ">=0.7.0" [[package]] name = "registry_schemas" -version = "2.18.62" +version = "2.18.64" description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -2131,8 +2131,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.62" -resolved_reference = "1cea81cf14104fb7d4989169236eff13b3df16c4" +reference = "2.18.64" +resolved_reference = "18b577402737130667e275d78a64e7a1ab3c9751" [[package]] name = "requests" diff --git a/queue_services/business-filer/pyproject.toml b/queue_services/business-filer/pyproject.toml index 84049a43ef..c1ce19c260 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.11" +version = "3.0.12" 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/filing_processors/filing_components/correction.py b/queue_services/business-filer/src/business_filer/filing_processors/filing_components/correction.py index 7fbf14e8fc..468be1c2b5 100644 --- a/queue_services/business-filer/src/business_filer/filing_processors/filing_components/correction.py +++ b/queue_services/business-filer/src/business_filer/filing_processors/filing_components/correction.py @@ -53,6 +53,12 @@ shares, update_address, ) +from business_filer.filing_processors.filing_components.relationships import ( + cease_relationships, + create_relationships, + update_relationship_addresses, + update_relationship_entity_info, +) CEASE_ROLE_MAPPING = { **dict.fromkeys(Business.CORPS, PartyRole.RoleTypes.DIRECTOR.value), @@ -114,6 +120,22 @@ def correct_business_data(business: Business, # noqa: PLR0915 party_json = dpath.get(correction_filing, "/correction/parties") update_parties(business, party_json, correction_filing_rec) + # Update relationships (newer schema for parties) + with suppress(IndexError, KeyError, TypeError): + relationships = dpath.get(correction_filing, "/correction/relationships") + create_relationships(relationships, business, correction_filing_rec) + cease_relationships(relationships, + business, + [ + PartyRole.RoleTypes.DIRECTOR.value, + PartyRole.RoleTypes.LIQUIDATOR.value, + PartyRole.RoleTypes.RECEIVER.value + ], + filing_meta.application_date) + update_relationship_addresses(relationships, business) + update_relationship_entity_info(relationships, business) + _set_lear_only(correction_filing, correction_filing_rec, relationships, business) + # update court order, if any is present with suppress(IndexError, KeyError, TypeError): court_order_json = dpath.get(correction_filing, "/correction/courtOrder") @@ -276,3 +298,29 @@ def _update_addresses(offices_structure): address = Address.find_by_id(updated_address.get("id")) if address: update_address(address, updated_address) + + +def _set_lear_only(correction_filing: dict, filing_rec: Filing, relationships: list[dict], business: Business): + """Set lear_only if the only changes are to receivers and/or liquidators.""" + def _has_director_role(relationship: dict): + """Return True if the relationship contains a director role.""" + return any(role for role in relationship["roles"] if role["roleType"].lower() == "director") + + if ( + ( + not any(( + # below are the only changes the colin api supports for corrections + bool(dpath.get(correction_filing, "/correction/nameRequest", default=None)), + bool(dpath.get(correction_filing, "/correction/nameTranslations", default=None)), + bool(dpath.get(correction_filing, "/correction/offices", default=None)), + bool(dpath.get(correction_filing, "/correction/parties", default=None)), + bool(dpath.get(correction_filing, "/correction/shareStructure", default=None)), + bool(dpath.get(correction_filing, "/correction/resolution", default=None))) + )) and ( + relationships and + # colin-api only supports relationships changes to directors + not any(relationship for relationship in relationships if _has_director_role(relationship)) + ) + ): + filing_rec.lear_only = True + \ No newline at end of file diff --git a/queue_services/business-filer/tests/unit/test_filer/test_correction.py b/queue_services/business-filer/tests/unit/test_filer/test_correction.py new file mode 100644 index 0000000000..2440fa467a --- /dev/null +++ b/queue_services/business-filer/tests/unit/test_filer/test_correction.py @@ -0,0 +1,139 @@ +# 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. +"""The Test Suite to ensure that the worker is operating correctly for corrections.""" +import copy +import pytest +import random + +from business_model.models import Business, Filing, PartyRole +from registry_schemas.example_data import ( + CHANGE_OF_DIRECTORS, + CHANGE_OF_RECEIVERS, + CHANGE_OF_LIQUIDATORS, + CORRECTION_COL, + CORRECTION_COR, + 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 + + +COD = copy.deepcopy(CHANGE_OF_DIRECTORS) +COD['directors'][0]['actions'] = ['appointed'] +COD['directors'][1]['actions'] = ['appointed'] + +CORRECTION_COD = copy.deepcopy(CORRECTION_COL) +CORRECTION_COD['filing']['correction']['correctedFilingType'] = 'changeOfDirectors' +CORRECTION_COD['filing']['correction']['relationships'][0]['roles'][0]['roleType'] = 'Director' + + +def _assert_common_data(business: Business, filing: Filing): + """Assert the expected common data was updated by the filing processing.""" + assert filing.transaction_id + assert filing.business_id == business.id + assert filing.status == Filing.Status.COMPLETED.value + + +def _get_filing(filing_type: str, data: dict, identifier = 'BC1234567', needs_template = True): + """Return the filing json, payment id and identifier.""" + payment_id = str(random.SystemRandom().getrandbits(0x58)) + if needs_template: + filing = copy.deepcopy(FILING_TEMPLATE) + filing['filing'][filing_type] = copy.deepcopy(data) + else: + filing = copy.deepcopy(data) + + filing['filing']['header']['name'] = filing_type + filing['filing']['business']['identifier'] = identifier + filing['filing']['business']['legalType'] = 'BC' + return filing, payment_id, identifier + + +@pytest.mark.parametrize('filing_name, original_data, correction_data, expected_lear_only', [ + ('changeOfDirectors', COD, CORRECTION_COD, False), + ('changeOfLiquidators', CHANGE_OF_LIQUIDATORS, CORRECTION_COL, True), + ('changeOfReceivers', CHANGE_OF_RECEIVERS, CORRECTION_COR, True), +]) +def test_process_correction_filing_with_relationships(app, session, mocker, filing_name, original_data, correction_data, expected_lear_only): + """Assert that correction filings can be applied to the model correctly.""" + # mock out the email sender and event publishing + mocker.patch('business_filer.services.publish_event.PublishEvent.publish_email_message', return_value=None) + mocker.patch('business_filer.services.publish_event.PublishEvent.publish_event', return_value=None) + mocker.patch('business_filer.filing_processors.filing_components.business_profile.update_business_profile', + return_value=None) + mocker.patch('business_filer.services.AccountService.update_entity', return_value=None) + + orig_filing, payment_id, identifier = _get_filing(filing_name, original_data) + business = create_business(identifier) + orig_filing_rec = create_filing(payment_id, orig_filing, business.id) + + # process original filing + filing_msg = FilingMessage(filing_identifier=orig_filing_rec.id) + process_filing(filing_msg) + orig_processed_filing: Filing = Filing.find_by_id(orig_filing_rec.id) + # sanity checks + _assert_common_data(business, orig_processed_filing) + party_roles = business.party_roles.all() + assert len(party_roles) == 2 + + # setup correction + correction_filing, corrected_payment_id, _ = _get_filing('correction', correction_data, business.identifier, False) + expected_given_name = 'corrected given name' + expected_mailing_street = 'corrected mailing street' + expected_delivery_street = 'corrected delivery street' + corrected_party_id = party_roles[0].party.id + correction_filing['filing']['correction']['correctedFilingId'] = orig_filing_rec.id + correction_filing['filing']['correction']['relationships'][0]['entity']['identifier'] = corrected_party_id + correction_filing['filing']['correction']['relationships'][0]['entity']['givenName'] = expected_given_name + correction_filing['filing']['correction']['relationships'][0]['mailingAddress']['streetAddress'] = expected_mailing_street + correction_filing['filing']['correction']['relationships'][0]['deliveryAddress']['streetAddress'] = expected_delivery_street + + correction_filing_rec = create_filing(corrected_payment_id, correction_filing, business.id) + + # process original filing + correction_filing_msg = FilingMessage(filing_identifier=correction_filing_rec.id) + process_filing(correction_filing_msg) + correction_processed_filing: Filing = Filing.find_by_id(correction_filing_rec.id) + # assert changes + _assert_common_data(business, correction_processed_filing) + assert correction_processed_filing.lear_only == expected_lear_only + party_roles: list[PartyRole] = business.party_roles.all() + assert len(party_roles) == 2 + for role in party_roles: + if role.party.id == corrected_party_id: + assert role.party.first_name == expected_given_name.upper() + assert role.party.mailing_address.street == expected_mailing_street + assert role.party.delivery_address.street == expected_delivery_street diff --git a/queue_services/business-filer/tests/unit/test_filer/test_correction_bcia.py b/queue_services/business-filer/tests/unit/test_filer/test_correction_bcia.py index 725515ad67..0a7de2f821 100644 --- a/queue_services/business-filer/tests/unit/test_filer/test_correction_bcia.py +++ b/queue_services/business-filer/tests/unit/test_filer/test_correction_bcia.py @@ -697,6 +697,8 @@ def tests_filer_resolution_dates_change(app, session, mocker, test_name, legal_t # Check outcome business = Business.find_by_internal_id(business_id) + filing = Filing.find_by_id(filing_id) + assert filing.lear_only == False resolution_dates = [res.resolution_date for res in business.resolutions.all()] if 'add_resolution_dates' in test_name: @@ -839,6 +841,8 @@ def tests_filer_share_class_and_series_change(app, session, mocker, test_name, l # Check outcome business = Business.find_by_internal_id(business_id) + filing = Filing.find_by_id(filing_id) + assert filing.lear_only == False if 'add_share_class' in test_name: assert len(business.share_classes.all()) == 4 @@ -912,6 +916,7 @@ def test_comment_only_correction(app, session, mocker, test_name): process_filing(filing_msg) final_filing = Filing.find_by_id(filing_id) + assert final_filing.lear_only == False meta_data = final_filing.meta_data.get('correction', {}) assert meta_data.get('commentOnly')