diff --git a/legal-api/pyproject.toml b/legal-api/pyproject.toml index af12fc9f7f..d4476b7c4c 100644 --- a/legal-api/pyproject.toml +++ b/legal-api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "legal-api" -version = "3.1.24" +version = "3.1.25" description = "" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} diff --git a/legal-api/src/legal_api/resources/v2/business/business_court_orders.py b/legal-api/src/legal_api/resources/v2/business/business_court_orders.py index 748ba2c864..0d53649e25 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_court_orders.py +++ b/legal-api/src/legal_api/resources/v2/business/business_court_orders.py @@ -18,6 +18,7 @@ from flask_cors import cross_origin from business_model.models import Business, CourtOrder, Filing +from business_model.models.types.filings import FilingTypes from legal_api.services import authorized from legal_api.utils.auth import jwt @@ -47,24 +48,28 @@ def get_court_orders(identifier, court_order_id=None): return jsonify(court_order), code court_orders_list = CourtOrder.get_json_with_filing_type(business.id) + for court_order in court_orders_list: + _include_court_order_files(court_order, business) return jsonify({ "courtOrders": court_orders_list }), HTTPStatus.OK -def _get_court_order(business, court_order_id=None): +def _get_court_order(business, court_order_id): if court_order := CourtOrder.get_by_id(court_order_id): court_order_json = court_order.json - filing = Filing.find_by_id(court_order.filing_id) - if filing.filing_type == "courtOrder": - _include_court_order_files(court_order_json, filing, business) + _include_court_order_files(court_order_json, business) return {"courtOrder": court_order_json}, HTTPStatus.OK return {"message": f"{business.identifier} court order not found"}, HTTPStatus.NOT_FOUND -def _include_court_order_files(court_order_json, filing, business): +def _include_court_order_files(court_order_json, business): + filing = Filing.find_by_id(court_order_json["filingId"]) + if filing.filing_type != FilingTypes.COURTORDER: + return + if documents := filing.documents.all(): base_url = current_app.config.get("BUSINESS_API_GW_URL") doc_url = url_for( diff --git a/legal-api/src/legal_api/services/filings/validations/correction.py b/legal-api/src/legal_api/services/filings/validations/correction.py index c4b6e10146..b2c8d8bffe 100644 --- a/legal-api/src/legal_api/services/filings/validations/correction.py +++ b/legal-api/src/legal_api/services/filings/validations/correction.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Validation for the Correction filing.""" -from datetime import timedelta +from datetime import UTC, datetime, timedelta from http import HTTPStatus from typing import Final @@ -45,6 +45,7 @@ validate_continuation_in_expro_business_in_colin, validate_continuation_in_foreign_jurisdiction, ) +from legal_api.services.filings.validations.dissolution import validate_custodian_email from legal_api.services.filings.validations.incorporation_application import ( validate_coop_parties_mailing_address, validate_roles, @@ -97,21 +98,6 @@ def validate(business: Business, filing: dict) -> Error: if not is_comment_only_correction: if filing.get("filing", {}).get("correction", {}).get("parties", None): msg.extend(validate_parties_addresses(filing, filing_type)) - if filing.get("filing", {}).get("correction", {}).get("relationships", None): - msg.extend(validate_relationships( - business, - filing, - filing_type, - [ - PartyRole.RoleTypes.DIRECTOR, - PartyRole.RoleTypes.LIQUIDATOR, - PartyRole.RoleTypes.RECEIVER, - PartyRole.RoleTypes.COMPLETING_PARTY - ], - True, - True, - [PartyRole.RoleTypes.DIRECTOR, PartyRole.RoleTypes.COMPLETING_PARTY] - )) if filing.get("filing", {}).get("correction", {}).get("offices", None): msg.extend(validate_offices_addresses(filing, filing_type)) @@ -145,6 +131,81 @@ def _validate_firms_correction(business: Business, filing, legal_type, msg): def _validate_corps_correction(business: Business, filing_dict, legal_type, msg): + if filing_dict.get("filing", {}).get("correction", {}).get("courtOrder", None): + msg.extend(court_order_validation(filing_dict)) + msg.extend(_validate_court_orders_correction(filing_dict, business)) + + if relationships := filing_dict.get("filing", {}).get("correction", {}).get("relationships", None): + relationships_path = "/filing/correction/relationships" + completing_parties = [ + x for x in relationships + if any( + role for role in x.get("roles", []) + if role["roleType"].lower().replace(" ", "_") == PartyRole.RoleTypes.COMPLETING_PARTY.value + ) + ] + correction_type = filing_dict.get("filing").get("correction").get("type", "STAFF") + if correction_type == "STAFF": + if len(completing_parties) != 0: + msg.append({ + "error": "Should not provide completing party when correction type is STAFF", + "path": relationships_path + }) + elif len(completing_parties) == 0: + msg.append({"error": "Completing party is required.", "path": relationships_path}) + elif len(completing_parties) > 1: + msg.append({"error": "Only one completing party is allowed.", "path": relationships_path}) + + if business.state == Business.State.HISTORICAL.value: + _validate_corps_correction_historical(business, filing_dict, msg) + else: + _validate_corps_correction_active(business, filing_dict, legal_type, msg) + + +def _validate_corps_correction_historical(business: Business, filing_dict, msg): + filing_type = "correction" + msg.extend(_validate_out_correction(filing_dict, filing_type, business)) + if relationships := filing_dict.get("filing", {}).get("correction", {}).get("relationships", None): + custodian_parties = [ + x for x in relationships + if any( + role for role in x.get("roles", []) + if role["roleType"].lower() == PartyRole.RoleTypes.CUSTODIAN.value + ) + ] + relationships_path = "/filing/correction/relationships" + if len(custodian_parties) > 1: + msg.append({"error": "Only one custodian is allowed.", "path": relationships_path}) + elif len(custodian_parties) == 1: + today = datetime.now(tz=UTC).date() + existing_custodian = PartyRole.get_party_roles(business.id, today, PartyRole.RoleTypes.CUSTODIAN.value) + if not custodian_parties[0].get("entity", {}).get("identifier") and len(existing_custodian) > 0: + msg.append({ + "error": "Custodian already exists for this business, cannot create another custodian.", + "path": relationships_path + }) + msg.extend( + validate_custodian_email( + custodian_parties[0].get("entity", {}).get("email"), + f"{relationships_path}/entity/email" + ) + ) + + msg.extend(validate_relationships( + business, + filing_dict, + filing_type, + [ + PartyRole.RoleTypes.CUSTODIAN, + PartyRole.RoleTypes.COMPLETING_PARTY + ], + True, + True, + [PartyRole.RoleTypes.CUSTODIAN, PartyRole.RoleTypes.COMPLETING_PARTY] + )) + + +def _validate_corps_correction_active(business: Business, filing_dict, legal_type, msg): filing_type = "correction" if new_legal_type := filing_dict.get("filing", {}).get("correction", {}).get("newLegalType"): if business.legal_type == new_legal_type: @@ -163,6 +224,23 @@ def _validate_corps_correction(business: Business, filing_dict, legal_type, msg) msg.extend(err) # FUTURE: this should be removed when COLIN sync back is no longer required. msg.extend(validate_parties_names(filing_dict, filing_type, legal_type)) + + if filing_dict.get("filing", {}).get("correction", {}).get("relationships", None): + msg.extend(validate_relationships( + business, + filing_dict, + filing_type, + [ + PartyRole.RoleTypes.DIRECTOR, + PartyRole.RoleTypes.LIQUIDATOR, + PartyRole.RoleTypes.RECEIVER, + PartyRole.RoleTypes.COMPLETING_PARTY + ], + True, + True, + [PartyRole.RoleTypes.DIRECTOR, PartyRole.RoleTypes.COMPLETING_PARTY] + )) + if filing_dict.get("filing", {}).get("correction", {}).get("shareStructure", None): err = validate_share_structure(filing_dict, filing_type, legal_type) if err: @@ -171,13 +249,8 @@ def _validate_corps_correction(business: Business, filing_dict, legal_type, msg) msg.extend(validate_share_currency(filing_dict, filing_type, business)) msg.extend(validate_resolution_date_in_share_structure(filing_dict, filing_type, business)) - if filing_dict.get("filing", {}).get("correction", {}).get("courtOrder", None): - msg.extend(court_order_validation(filing_dict)) - msg.extend(_validate_continuation_in_correction(filing_dict, filing_type, legal_type, business)) - msg.extend(_validate_out_correction(filing_dict, filing_type, business)) msg.extend(_validate_amalgamation_correction(filing_dict, filing_type, business)) - msg.extend(_validate_court_orders_correction(filing_dict, business)) def _validate_court_orders_correction(filing_dict, business: Business): diff --git a/legal-api/src/legal_api/services/filings/validations/dissolution.py b/legal-api/src/legal_api/services/filings/validations/dissolution.py index 505bcc22a6..bc88e5a447 100644 --- a/legal-api/src/legal_api/services/filings/validations/dissolution.py +++ b/legal-api/src/legal_api/services/filings/validations/dissolution.py @@ -97,10 +97,7 @@ def validate(business: Business, dissolution: dict) -> Error | None: if err: msg.extend(err) - # Specific validation for addresses in dissolution - err = validate_dissolution_parties_address(dissolution, business.legal_type, dissolution_type) - if err: - msg.extend(err) + msg.extend(validate_dissolution_parties_address(dissolution, business.legal_type, dissolution_type)) if dissolution["filing"]["dissolution"].get("parties"): # Common validation for addresses @@ -253,31 +250,39 @@ def validate_dissolution_parties_address(filing_json, legal_type, dissolution_ty This needs not to be validated for SP and GP This needs not to be validated for administrative dissolution """ - if dissolution_type in [DissolutionTypes.ADMINISTRATIVE, DissolutionTypes.DELAY]: - return None + msg = [] + if ( + dissolution_type in [DissolutionTypes.ADMINISTRATIVE, DissolutionTypes.DELAY] or + legal_type in [Business.LegalTypes.SOLE_PROP.value, Business.LegalTypes.PARTNERSHIP.value] or + "parties" not in filing_json["filing"]["dissolution"] + ): + return msg - if legal_type in [Business.LegalTypes.SOLE_PROP.value, Business.LegalTypes.PARTNERSHIP.value]: - return None + parties_json = filing_json["filing"]["dissolution"]["parties"] + custodian_count = 0 - if "parties" not in filing_json["filing"]["dissolution"]: - return None + for idx, party in enumerate(parties_json): + if _is_custodian_role(party.get("roles", [])): + custodian_count += 1 + path = f"/filing/dissolution/parties/{idx}" + if legal_type in Business.CORPS and dissolution_type == DissolutionTypes.VOLUNTARY.value: + # only validate email for CORP voluntary dissolution + email = party.get("officer", {}).get("email") + msg.extend(validate_custodian_email(email, f"{path}/officer/email")) + msg.extend(_validate_custodian_name(party, path)) - parties_json = filing_json["filing"]["dissolution"]["parties"] - custodians = list(filter(lambda x: _is_custodian_role(x.get("roles", [])), parties_json)) + for address_type in Address.JSON_ADDRESS_TYPES: + msg.extend(_validate_party_address(party, idx, address_type, legal_type in Business.CORPS)) - if not custodians: + if custodian_count == 0: # Handle case where there are no custodians, but there is a liquidator role # (this is not implemented in the Create UI, keeping behavior here) if any(_is_liquidator_role(p.get("roles", [])) for p in parties_json): - return None - return [{"error": "Dissolution party is required.", "path": "/filing/dissolution/parties"}] - - msg = [] - msg.extend(_validate_custodian_email(custodians, dissolution_type, legal_type)) - msg.extend(_validate_custodian_name(custodians, dissolution_type, legal_type)) - msg.extend(_validate_address_location(custodians, legal_type)) + return msg + msg.append({"error": "Dissolution party is required.", "path": "/filing/dissolution/parties"}) + return msg - return msg or None + return msg def _is_custodian_role(roles: list) -> bool: @@ -290,16 +295,6 @@ def _is_liquidator_role(roles: list) -> bool: for role in roles) -def _validate_address_location(parties, legal_type): - """Every party address must be in Canada; CORP types also require the BC province.""" - msg = [] - require_bc = legal_type in Business.CORPS - for idx, party in enumerate(parties): - for address_type in Address.JSON_ADDRESS_TYPES: - msg.extend(_validate_party_address(party, idx, address_type, require_bc)) - return msg - - def _validate_party_address(party, idx, address_type, require_bc): address_path = f"/filing/dissolution/parties/{idx}/{address_type}" if address_type not in party: @@ -356,58 +351,50 @@ def _validate_court_order(filing): return [] -def _validate_custodian_email(parties, dissolution_type, legal_type) -> list: +def validate_custodian_email(email, path) -> list: """Validate custodian email for voluntary dissolution.""" - # Only validate for CORP voluntary dissolution - if not (legal_type in Business.CORPS and dissolution_type == DissolutionTypes.VOLUNTARY.value): - return [] - msg = [] - for idx, party in enumerate(parties): - email = get_str(party, "/officer/email") - if not email: - msg.append({"error": "Custodian email is required for voluntary dissolution.", - "path": f"/filing/dissolution/parties/{idx}/officer/email"}) - elif any(char.isspace() for char in email): - msg.append({ - "error": "Custodian email cannot contain any whitespaces.", - "path": f"/filing/dissolution/parties/{idx}/officer/email" - }) + if not email: + msg.append({"error": "Custodian email is required.", + "path": path}) + elif any(char.isspace() for char in email): + msg.append({ + "error": "Custodian email cannot contain any whitespaces.", + "path": path + }) return msg -def _validate_custodian_name(parties, dissolution_type, legal_type) -> list: - """Validate custodian name of the dissolution filing and trim it.""" - # Only validate for CORP voluntary dissolution - if not (legal_type in Business.CORPS and dissolution_type == DissolutionTypes.VOLUNTARY.value): - return [] +def _validate_custodian_name(custodian, path) -> list: + """Validate custodian name of the dissolution filing and trim it.""" msg = [] - for idx, party in enumerate(parties): - party_type = get_str(party, "/officer/partyType") - # Organization custodian name (required + no surrounding whitespace) is enforced by the - # schema (business-schemas parties officer.organizationName pattern). firstName is not - # schema-patterned, so it is still validated here. - if party_type != "organization": - first_name = get_str(party, "/officer/firstName") - - if first_name is None or not first_name.strip(): - msg.append({ - "error": "Custodian first name is required.", - "path": f"/filing/dissolution/parties/{idx}/officer/firstName" + party_type = get_str(custodian, "/officer/partyType") + # Organization custodian name (required + no surrounding whitespace) is enforced by the + # schema (business-schemas parties officer.organizationName pattern). + # firstName is not schema-patterned, so it is still validated here. + if party_type != "organization": + first_name = get_str(custodian, "/officer/firstName") + + if first_name is None or not first_name.strip(): + msg.append({ + "error": "Custodian first name is required.", + "path": f"{path}/officer/firstName" }) - elif first_name != first_name.strip(): - msg.append({ - "error": "Custodian first name cannot have leading or trailing spaces.", - "path": f"/filing/dissolution/parties/{idx}/officer/firstName" + elif first_name != first_name.strip(): + msg.append({ + "error": "Custodian first name cannot have leading or trailing spaces.", + "path": f"{path}/officer/firstName" }) return msg + def _check_dissolution_permission(required_permission: str, dissolution_type: str, filing_type: str) -> Error | None: """Check if the user has the required permission for the dissolution filing.""" message = "Permission Denied - You do not have permissions file {dissolution_type} {filing_type} filing." return PermissionService.check_user_permission(required_permission, message=message) + def _validate_dissolution_permission(business: Business, dissolution: dict, dissolution_type: str, filing_type: str, msg: list) -> Error | None: """Validate dissolution permission based on business and dissolution type.""" @@ -449,4 +436,4 @@ def _validate_dissolution_permission(business: Business, dissolution: dict, diss "check_email":True, "check_address":True, "check_document_email":True} - ) + ) diff --git a/legal-api/tests/unit/resources/v2/test_business_court_orders.py b/legal-api/tests/unit/resources/v2/test_business_court_orders.py index 231ce4908f..5c893f8eed 100644 --- a/legal-api/tests/unit/resources/v2/test_business_court_orders.py +++ b/legal-api/tests/unit/resources/v2/test_business_court_orders.py @@ -27,43 +27,8 @@ from tests.unit.services.utils import create_header -def test_get_business_court_orders(app, session, client, jwt, requests_mock): - """Assert that business court orders are returned.""" - identifier = 'CP1234567' - business = factory_business(identifier) - filing_dict = copy.deepcopy(FILING_TEMPLATE) - filing_dict['filing']['header']['name'] = 'courtOrder' - filing = factory_completed_filing(business, filing_dict) - - court_order = CourtOrder( - file_number='123456', - order_date='2021-01-31T00:00:00+00:00', - effect_of_order='planOfArrangement', - business_id=business.id, - filing_id=filing.id - ) - court_order.save() - - requests_mock.get(f"{app.config.get('AUTH_SVC_URL')}/entities/{identifier}/authorizations", json={'roles': ['view']}) - - rv = client.get(f'/api/v2/businesses/{identifier}/court-orders', - headers=create_header(jwt, [STAFF_ROLE], identifier) - ) - - assert rv.status_code == HTTPStatus.OK - assert 'courtOrders' in rv.json - assert len(rv.json['courtOrders']) == 1 - assert rv.json['courtOrders'][0]['fileNumber'] == '123456' - - -def test_get_business_court_order_by_id(app, session, client, jwt, requests_mock): - """Assert that a specific business court order is returned.""" - identifier = 'CP1234567' - business = factory_business(identifier) - filing_dict = copy.deepcopy(FILING_TEMPLATE) - filing_dict['filing']['header']['name'] = 'courtOrder' - filing = factory_completed_filing(business, filing_dict) - +base_url = 'https://LEGAL_API_BASE_URL' +def create_court_order_and_documents(business, filing): court_order = CourtOrder( file_number='123456', order_date='2021-01-31T00:00:00+00:00', @@ -94,6 +59,57 @@ def test_get_business_court_order_by_id(app, session, client, jwt, requests_mock ) document.save() + files = [ + { + 'fileName': file_name, + 'fileKey': file_key, + 'documentType': DocumentType.COURT_ORDER.value, + 'url': f'{base_url}/api/v2/businesses/{business.identifier}/filings/{filing.id}/documents/static/{file_key}' + }, + { + 'fileName': file_name_2, + 'fileKey': file_key_2, + 'documentType': DocumentType.SUPPORTING_DOCUMENT.value, + 'url': f'{base_url}/api/v2/businesses/{business.identifier}/filings/{filing.id}/documents/static/{file_key_2}' + } + ] + + return court_order, files + + +def test_get_business_court_orders(app, session, client, jwt, requests_mock): + """Assert that business court orders are returned.""" + identifier = 'CP1234567' + business = factory_business(identifier) + filing_dict = copy.deepcopy(FILING_TEMPLATE) + filing_dict['filing']['header']['name'] = 'courtOrder' + filing = factory_completed_filing(business, filing_dict) + + court_order, files = create_court_order_and_documents(business, filing) + + requests_mock.get(f"{app.config.get('AUTH_SVC_URL')}/entities/{identifier}/authorizations", json={'roles': ['view']}) + + rv = client.get(f'/api/v2/businesses/{identifier}/court-orders', + headers=create_header(jwt, [STAFF_ROLE], identifier) + ) + + assert rv.status_code == HTTPStatus.OK + assert 'courtOrders' in rv.json + assert len(rv.json['courtOrders']) == 1 + assert rv.json['courtOrders'][0]['fileNumber'] == '123456' + assert rv.json['courtOrders'][0]['files'] == files + + +def test_get_business_court_order_by_id(app, session, client, jwt, requests_mock): + """Assert that a specific business court order is returned.""" + identifier = 'CP1234567' + business = factory_business(identifier) + filing_dict = copy.deepcopy(FILING_TEMPLATE) + filing_dict['filing']['header']['name'] = 'courtOrder' + filing = factory_completed_filing(business, filing_dict) + + court_order, files = create_court_order_and_documents(business, filing) + requests_mock.get(f"{app.config.get('AUTH_SVC_URL')}/entities/{identifier}/authorizations", json={'roles': ['view']}) rv = client.get(f'/api/v2/businesses/{identifier}/court-orders/{court_order.id}', @@ -103,18 +119,7 @@ def test_get_business_court_order_by_id(app, session, client, jwt, requests_mock assert rv.status_code == HTTPStatus.OK assert 'courtOrder' in rv.json assert rv.json['courtOrder']['fileNumber'] == '123456' - assert 'files' in rv.json['courtOrder'] - assert len(rv.json['courtOrder']['files']) == 2 - assert rv.json['courtOrder']['files'][0]['fileName'] == file_name - assert rv.json['courtOrder']['files'][0]['fileKey'] == file_key - assert rv.json['courtOrder']['files'][0]['documentType'] == DocumentType.COURT_ORDER.value - assert rv.json['courtOrder']['files'][0]['url'].endswith( - f'api/v2/businesses/{identifier}/filings/{filing.id}/documents/static/{file_key}') - assert rv.json['courtOrder']['files'][1]['fileName'] == file_name_2 - assert rv.json['courtOrder']['files'][1]['fileKey'] == file_key_2 - assert rv.json['courtOrder']['files'][1]['documentType'] == DocumentType.SUPPORTING_DOCUMENT.value - assert rv.json['courtOrder']['files'][1]['url'].endswith( - f'api/v2/businesses/{identifier}/filings/{filing.id}/documents/static/{file_key_2}') + assert rv.json['courtOrder']['files'] == files def test_get_business_court_orders_not_found(app, session, client, jwt, requests_mock): diff --git a/legal-api/tests/unit/services/filings/validations/test_correction.py b/legal-api/tests/unit/services/filings/validations/test_correction.py index f671709f03..905f31545f 100644 --- a/legal-api/tests/unit/services/filings/validations/test_correction.py +++ b/legal-api/tests/unit/services/filings/validations/test_correction.py @@ -57,8 +57,9 @@ def test_valid_correction(session, app, jwt, test_name, legal_type, identifier, f = copy.deepcopy(correction_filing) f['filing']['header']['identifier'] = identifier f['filing']['correction']['correctedFilingId'] = corrected_filing.id - f['filing']['correction']['type'] = "CLIENT" + f['filing']['correction']['type'] = "STAFF" if test_name == 'COD': + f['filing']['correction']['type'] = "CLIENT" f['filing']['correction']['relationships'].append({ 'entity': { 'givenName': 'Phillip Tandy', @@ -168,7 +169,7 @@ def test_correction__invalid_director_dates(session, app, jwt): f = copy.deepcopy(CORRECTION_COD) f['filing']['header']['identifier'] = identifier f['filing']['correction']['correctedFilingId'] = corrected_filing.id - f['filing']['correction']['type'] = "CLIENT" + f['filing']['correction']['type'] = "STAFF" # Set appointment date after cessation date f['filing']['correction']['relationships'][0]['roles'][0]['appointmentDate'] = '2025-02-01' @@ -217,7 +218,7 @@ def test_validate_relationship_date(session, app, jwt, date_label, test_name, of f = copy.deepcopy(CORRECTION_COD) f['filing']['header']['identifier'] = identifier f['filing']['correction']['correctedFilingId'] = corrected_filing.id - f['filing']['correction']['type'] = "CLIENT" + f['filing']['correction']['type'] = "STAFF" # Set appointment date after cessation date diff --git a/legal-api/tests/unit/services/filings/validations/test_correction_ia.py b/legal-api/tests/unit/services/filings/validations/test_correction_ia.py index a42db2530e..40d9d56992 100644 --- a/legal-api/tests/unit/services/filings/validations/test_correction_ia.py +++ b/legal-api/tests/unit/services/filings/validations/test_correction_ia.py @@ -23,7 +23,7 @@ import pytest -from business_model.models import AmalgamatingBusiness, Amalgamation, Business, CourtOrder, Resolution +from business_model.models import AmalgamatingBusiness, Amalgamation, Business, CourtOrder, PartyRole, Resolution from business_common.utils.legislation_datetime import LegislationDatetime from business_common.utils.datetime import datetime as dt, timedelta from legal_api.services import NameXService @@ -41,7 +41,13 @@ ) from tests.unit import MockResponse -from tests.unit.models import factory_business, factory_completed_filing, factory_jurisdiction +from tests.unit.models import ( + factory_address, + factory_business, + factory_completed_filing, + factory_jurisdiction, + factory_party_role +) from tests.unit.services.filings.validations import lists_are_equal from tests.unit.services.utils import jwt_request_context @@ -900,6 +906,8 @@ def test_validate_continuation_out_date(session, app, jwt, filing_type, test_nam """Assert validate continuation_out_date.""" identifier = 'BC1234567' business = factory_business(identifier, entity_type='BC') + business.state = Business.State.HISTORICAL.value + business.save() continuation_out_filing = copy.deepcopy(FILING_HEADER) continuation_out_filing['filing'][filing_type] = copy.deepcopy(CONTINUATION_OUT if filing_type == 'continuationOut' else AMALGAMATION_OUT) continuation_out_filing['filing']['header']['name'] = filing_type @@ -950,6 +958,8 @@ def test_validate_continuation_out_foreign_jurisdiction(session, app, jwt, filin """Assert validate continuation_out foreign jurisdiction.""" identifier = 'BC1234567' business = factory_business(identifier, entity_type='BC') + business.state = Business.State.HISTORICAL.value + business.save() continuation_out_filing = copy.deepcopy(FILING_HEADER) continuation_out_filing['filing'][filing_type] = copy.deepcopy(CONTINUATION_OUT if filing_type == 'continuationOut' else AMALGAMATION_OUT) continuation_out_filing['filing']['header']['name'] = filing_type @@ -1006,6 +1016,7 @@ def test_validate_correction_out_existing_foreign_jurisdiction(mocker, app, sess identifier = 'BC1234567' business = factory_business(identifier, entity_type='BC') business.jurisdiction = 'UNKNOWN' + business.state = Business.State.HISTORICAL.value business.save() continuation_out_filing = copy.deepcopy(FILING_HEADER) continuation_out_filing['filing'][filing_type] = copy.deepcopy(CONTINUATION_OUT if filing_type == 'continuationOut' else AMALGAMATION_OUT) @@ -1467,3 +1478,133 @@ def test_validate_invalid_court_orders(app, jwt, session, test_name, error_msg): err = validate(business, filing) assert err assert err.msg[0]['error'] == error_msg + + +@pytest.mark.parametrize('test_name, err_msg', [ + ("test_completing_party_staff", "Should not provide completing party when correction type is STAFF"), + ("test_multiple_completing_parties", 'Only one completing party is allowed.'), + ("test_no_completing_parties", 'Completing party is required.'), + ("test_multiple_custodians", "Only one custodian is allowed."), + ("test_existing_custodian", "Custodian already exists for this business, cannot create another custodian.") +]) +def test_validate_correction_relationships_roles(session, app, jwt, test_name, err_msg): + """Test valid comment only IA validation.""" + # setup + identifier = 'BC1234567' + business = factory_business(identifier, entity_type='BC') + business.state = Business.State.HISTORICAL + business.save() + + if test_name == "test_existing_custodian": + custodian = factory_party_role( + delivery_address=factory_address('delivery street', 'delivery'), + mailing_address=factory_address('mailing street', 'mailing'), + appointment_date='2026-01-01', + cessation_date=None, + officer={ + 'firstName': 'first', + 'lastName': 'last', + 'middleInitial': 'mid', + 'partyType': 'person', + 'organizationName': '' + }, + role_type=PartyRole.RoleTypes.CUSTODIAN + ) + + custodian.business_id = business.id + session.add(custodian) + session.commit() + + corrected_filing = factory_completed_filing(business, INCORPORATION_APPLICATION) + + filing = copy.deepcopy(CORRECTION) + filing['filing']['header']['identifier'] = identifier + filing['filing']['correction']['correctedFilingId'] = corrected_filing.id + filing['filing']['business']['legalType'] = business.legal_type + del filing['filing']['correction']['commentOnly'] + del filing['filing']['correction']['parties'] + + if test_name == "test_completing_party_staff": + filing['filing']['correction']['type'] = 'STAFF' + filing['filing']['correction']['relationships'] = [] + if test_name != "test_no_completing_parties": + filing['filing']['correction']['relationships'].append({ + 'entity': { + 'givenName': 'Joe', + 'familyName': 'Swanson', + }, + 'mailingAddress': { + 'streetAddress': 'mailing_address - address line one', + 'streetAddressAdditional': '', + 'addressCity': 'mailing_address city', + 'addressCountry': 'CA', + 'postalCode': 'H0H0H0', + 'addressRegion': 'BC' + }, + 'deliveryAddress': { + 'streetAddress': 'delivery_address - address line one', + 'streetAddressAdditional': '', + 'addressCity': 'delivery_address city', + 'addressCountry': 'CA', + 'postalCode': 'H0H0H0', + 'addressRegion': 'BC' + }, + 'roles': [ + { + 'roleType': 'Completing Party', + 'appointmentDate': '2018-01-01' + + } + ] + }) + + if test_name == "test_multiple_completing_parties": + filing['filing']['correction']['relationships'].append(filing['filing']['correction']['relationships'][0]) + + if "custodian" in test_name or test_name == "test_no_completing_parties": + custodian_role = { + 'entity': { + 'givenName': 'first', + 'familyName': 'last', + 'email': 'test@test.com' + }, + 'mailingAddress': { + 'streetAddress': 'mailing_address - address line one', + 'streetAddressAdditional': '', + 'addressCity': 'mailing_address city', + 'addressCountry': 'CA', + 'postalCode': 'H0H0H0', + 'addressRegion': 'BC' + }, + 'deliveryAddress': { + 'streetAddress': 'delivery_address - address line one', + 'streetAddressAdditional': '', + 'addressCity': 'delivery_address city', + 'addressCountry': 'CA', + 'postalCode': 'H0H0H0', + 'addressRegion': 'BC' + }, + 'roles': [ + { + 'roleType': 'Custodian', + 'appointmentDate': '2018-01-01' + } + ] + } + if test_name == "test_multiple_custodians": + filing['filing']['correction']['relationships'].append(custodian_role) + # elif test_name == "test_existing_custodian": + # custodian_role["entity"]["identifier"] = str(custodian.party.id) + filing['filing']['correction']['relationships'].append(custodian_role) + + + with jwt_request_context(app, jwt, [STAFF_ROLE]): + if err := validate(business, filing): + print(err.msg) + + if not err_msg: + assert None is err + else: + assert err + assert HTTPStatus.BAD_REQUEST == err.code + assert err.msg[0]['error'] == err_msg diff --git a/legal-api/tests/unit/services/filings/validations/test_dissolution.py b/legal-api/tests/unit/services/filings/validations/test_dissolution.py index e858326c22..f4b464d383 100644 --- a/legal-api/tests/unit/services/filings/validations/test_dissolution.py +++ b/legal-api/tests/unit/services/filings/validations/test_dissolution.py @@ -283,7 +283,10 @@ def test_validate_dissolution_parties_address_location_per_address( 'deliveryAddress': {'addressCountry': country, 'addressRegion': region}, }] - result = dissolution._validate_address_location(parties, legal_type) + result = [] + for idx, party in enumerate(parties): + for address_type in ['deliveryAddress', 'mailingAddress']: + result.extend(dissolution._validate_party_address(party, idx, address_type, legal_type in Business.CORPS)) expected = per_address_errors * 2 assert [m['error'] for m in result] == expected @@ -442,13 +445,13 @@ def test_dissolution_court_orders(session, test_status, file_number, effect_of_o [ # Required email cases (missing or None) ('FAIL', 'BC', 'voluntary', None, HTTPStatus.BAD_REQUEST, - 'Custodian email is required for voluntary dissolution.'), + 'Custodian email is required.'), ('FAIL', 'BEN', 'voluntary', None, HTTPStatus.BAD_REQUEST, - 'Custodian email is required for voluntary dissolution.'), + 'Custodian email is required.'), ('FAIL', 'CC', 'voluntary', None, HTTPStatus.BAD_REQUEST, - 'Custodian email is required for voluntary dissolution.'), + 'Custodian email is required.'), ('FAIL', 'ULC', 'voluntary', None, HTTPStatus.BAD_REQUEST, - 'Custodian email is required for voluntary dissolution.'), + 'Custodian email is required.'), # Whitespace-only emails ('FAIL', 'BC', 'voluntary', ' ', HTTPStatus.BAD_REQUEST,