From 391fff73325a1de25f9fd98e2eecb1806a235b66 Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Wed, 27 Nov 2024 11:15:37 -0800 Subject: [PATCH] MHR staff death transfer validation updates. (#2074) * MHR staff death transfer validation updates. Signed-off-by: Doug Lovett * MHR/PPR minor update to report request log messages. Signed-off-by: Doug Lovett --------- Signed-off-by: Doug Lovett --- mhr-api/pyproject.toml | 2 +- .../v1/registration_report_callback.py | 4 +- .../resources/v1/search_report_callback.py | 2 +- .../mhr_api/resources/v1/search_results.py | 2 +- mhr-api/src/mhr_api/utils/admin_validator.py | 8 +- .../mhr_api/utils/registration_validator.py | 350 +++------ .../mhr_api/utils/validator_owner_utils.py | 711 ++++++++++++++++++ mhr-api/src/mhr_api/utils/validator_utils.py | 379 ---------- .../tests/unit/utils/test_admin_validator.py | 6 +- .../unit/utils/test_registration_validator.py | 53 +- .../unit/utils/test_transfer_validator.py | 73 +- .../src/ppr_api/resources/financing_utils.py | 6 +- ppr-api/src/ppr_api/resources/v1/callbacks.py | 2 +- .../ppr_api/resources/v1/search_results.py | 4 +- 14 files changed, 890 insertions(+), 712 deletions(-) create mode 100644 mhr-api/src/mhr_api/utils/validator_owner_utils.py diff --git a/mhr-api/pyproject.toml b/mhr-api/pyproject.toml index 32997c39f..3ac5d9379 100644 --- a/mhr-api/pyproject.toml +++ b/mhr-api/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "mhr-api" -version = "2.0.4" +version = "2.0.5" description = "" authors = ["dlovett "] license = "BSD 3" diff --git a/mhr-api/src/mhr_api/resources/v1/registration_report_callback.py b/mhr-api/src/mhr_api/resources/v1/registration_report_callback.py index 750d50ce4..21b8927f0 100755 --- a/mhr-api/src/mhr_api/resources/v1/registration_report_callback.py +++ b/mhr-api/src/mhr_api/resources/v1/registration_report_callback.py @@ -152,14 +152,14 @@ def get_registration_callback_report( doc_name = model_utils.get_doc_storage_name(registration) logger.info(f"Saving registration report output to doc storage: name={doc_name}.") response = GoogleStorageService.save_document(doc_name, raw_data, DocumentTypes.REGISTRATION) - logger.info(f"Updating report tracking doc_storage_url to {doc_name}.") + logger.info(f"Updating report tracking doc_storage_url to {doc_name}, doc storage response {response}.") report_info.doc_storage_url = doc_name report_info.save() # Track success event. EventTracking.create( registration_id, EventTracking.EventTrackingTypes.MHR_REGISTRATION_REPORT, int(HTTPStatus.OK) ) - return response, HTTPStatus.OK + return {}, HTTPStatus.OK except ReportException as report_err: return registration_callback_error( resource_utils.CallbackExceptionCodes.REPORT_ERR, diff --git a/mhr-api/src/mhr_api/resources/v1/search_report_callback.py b/mhr-api/src/mhr_api/resources/v1/search_report_callback.py index f42a95c71..627aed865 100644 --- a/mhr-api/src/mhr_api/resources/v1/search_report_callback.py +++ b/mhr-api/src/mhr_api/resources/v1/search_report_callback.py @@ -104,7 +104,7 @@ def post_search_report_callback( # pylint: disable=too-many-branches,too-many-l doc_name = model_utils.get_search_doc_storage_name(search_detail.search) logger.info(f"Saving report output to doc storage: name={doc_name}.") response = GoogleStorageService.save_document(doc_name, raw_data) - logger.info("Save document storage response: " + str(response)) + logger.info(f"Save document storage response: {response}") search_detail.doc_storage_url = doc_name search_detail.save() diff --git a/mhr-api/src/mhr_api/resources/v1/search_results.py b/mhr-api/src/mhr_api/resources/v1/search_results.py index b31e31c9f..f478987a2 100644 --- a/mhr-api/src/mhr_api/resources/v1/search_results.py +++ b/mhr-api/src/mhr_api/resources/v1/search_results.py @@ -388,7 +388,7 @@ def generate_search_report(search_detail: SearchResult, search_id: str): doc_name = model_utils.get_search_doc_storage_name(search_detail.search) logger.info(f"Saving report output to doc storage: name={doc_name}.") response = GoogleStorageService.save_document(doc_name, raw_data) - logger.info("Save document storage response: " + str(response)) + logger.info(f"Save document storage response: {response}") search_detail.doc_storage_url = doc_name search_detail.save() diff --git a/mhr-api/src/mhr_api/utils/admin_validator.py b/mhr-api/src/mhr_api/utils/admin_validator.py index bde8ca792..601d49729 100644 --- a/mhr-api/src/mhr_api/utils/admin_validator.py +++ b/mhr-api/src/mhr_api/utils/admin_validator.py @@ -18,7 +18,7 @@ from mhr_api.models import MhrRegistration from mhr_api.models import registration_utils as reg_utils from mhr_api.models.type_tables import MhrDocumentTypes, MhrNoteStatusTypes, MhrRegistrationTypes -from mhr_api.utils import validator_utils +from mhr_api.utils import validator_utils, validator_owner_utils from mhr_api.utils.logging import logger NCAN_DOC_TYPES = " CAU CAUC CAUE NCON NPUB REGC REST " # Set of doc types NCAN can cancel. @@ -212,11 +212,11 @@ def validate_owners(registration: MhrRegistration, json_data: dict) -> str: error_msg += DELETE_OWNERS_MISSING if not json_data.get("addOwnerGroups") or not json_data.get("deleteOwnerGroups"): return error_msg - active_group_count: int = validator_utils.get_active_group_count(json_data, registration) + active_group_count: int = validator_owner_utils.get_active_group_count(json_data, registration) error_msg += validator_utils.validate_submitting_party(json_data) - error_msg += validator_utils.validate_owner_groups( + error_msg += validator_owner_utils.validate_owner_groups( json_data.get("addOwnerGroups"), False, registration, json_data.get("deleteOwnerGroups"), active_group_count ) if registration and json_data.get("deleteOwnerGroups"): - error_msg += validator_utils.validate_delete_owners(registration, json_data) + error_msg += validator_owner_utils.validate_delete_owners(registration, json_data) return error_msg diff --git a/mhr-api/src/mhr_api/utils/registration_validator.py b/mhr-api/src/mhr_api/utils/registration_validator.py index 81a69aa26..38ec21aa5 100644 --- a/mhr-api/src/mhr_api/utils/registration_validator.py +++ b/mhr-api/src/mhr_api/utils/registration_validator.py @@ -28,7 +28,7 @@ MhrTenancyTypes, ) from mhr_api.services.authz import DEALERSHIP_GROUP, GOV_ACCOUNT_ROLE, MANUFACTURER_GROUP, QUALIFIED_USER_GROUP -from mhr_api.utils import validator_utils +from mhr_api.utils import validator_owner_utils, validator_utils from mhr_api.utils.logging import logger OWNERS_NOT_ALLOWED = "Owners not allowed with new registrations: use ownerGroups instead. " @@ -43,22 +43,14 @@ MANUFACTURER_DEALER_INVALID = "The existing location must be a dealer or manufacturer lot for this registration. " MANUFACTURER_PERMIT_INVALID = "A manufacturer can only submit a transport permit once for a home. " PARTY_TYPE_INVALID = "Death of owner requires an executor, trustee, administrator owner party type. " -GROUP_PARTY_TYPE_INVALID = ( - "For TRUSTEE, ADMINISTRATOR, or EXECUTOR, all owner party types within the group " + "must be identical. " -) -OWNER_DESCRIPTION_REQUIRED = "Owner description is required for the owner party type. " -TRANSFER_PARTY_TYPE_INVALID = "Owner party type of administrator, executor, trustee not allowed for this registration. " -TENANCY_PARTY_TYPE_INVALID = "Owner group tenancy type must be NA for executors, trustees, or administrators. " -TENANCY_TYPE_NA_INVALID = "Tenancy type NA is not allowed when there is 1 active owner group with 1 owner. " REG_STAFF_ONLY = "Only BC Registries Staff are allowed to submit this registration. " -TRAN_DEATH_GROUP_COUNT = "Only one owner group can be modified in a transfer due to death registration. " TRAN_DEATH_JOINT_TYPE = "The existing tenancy type must be joint for this transfer registration. " TRAN_ADMIN_OWNER_INVALID = "The existing owners must be administrators for this registration. " TRAN_DEATH_OWNER_INVALID = "The owners must be individuals or businesses for this registration. " TRAN_EXEC_OWNER_INVALID = "The owners must be individuals, businesses, or executors for this registration. " -TRAN_ADMIN_NEW_OWNER = "The new owners must be administrators for this registration. " -TRAN_DEATH_NEW_OWNER = "The new owners must be individuals or businesses for this registration. " -TRAN_AFFIDAVIT_NEW_OWNER = "The new owners must be executors for this registration. " +TRAN_ADMIN_NEW_OWNER = "One of the new owners must be an administrator for this registration. " +TRAN_DEATH_NEW_OWNER = "One of the new owners must be an individual or business for this registration. " +TRAN_AFFIDAVIT_NEW_OWNER = "One of the new owners must be an executor for this registration. " TRAN_DEATH_ADD_OWNER = "Owners cannot be added with this registration. " TRAN_DEATH_CERT_MISSING = "A death certificate number is required with this registration. " TRAN_DEATH_CORP_NUM_MISSING = "A removed business owner corporation number is required with this registration. " @@ -72,7 +64,7 @@ TRAN_WILL_DEATH_CERT = ( "Deceased owners without a probate document must have a death certificate " + "or corporation number. " ) -TRAN_WILL_NEW_OWNER = "The new owners must be executors for this registration. " +TRAN_WILL_NEW_OWNER = "One of the new owners must be an executor for this registration. " TRAN_EXEC_DEATH_CERT = "All deceased owners must have a death certificate or corporation number. " TRAN_ADMIN_GRANT = "One (and only one) deceased owner must have a grant document (no death certificate). " TRAN_ADMIN_DEATH_CERT = "Deceased owners without a grant document must have a death certificate or corporation number. " @@ -148,8 +140,12 @@ def validate_registration(json_data, staff: bool = False): error_msg += validator_utils.validate_submitting_party(json_data) error_msg += validator_utils.validate_mhr_number(json_data.get("mhrNumber", ""), staff) owner_count: int = len(json_data.get("ownerGroups")) if json_data.get("ownerGroups") else 0 - error_msg += validator_utils.validate_owner_groups(json_data.get("ownerGroups"), True, None, None, owner_count) - error_msg += validate_owner_party_type(json_data, json_data.get("ownerGroups"), True, owner_count) + error_msg += validator_owner_utils.validate_owner_groups( + json_data.get("ownerGroups"), True, None, None, owner_count + ) + error_msg += validator_owner_utils.validate_owner_party_type( + json_data, json_data.get("ownerGroups"), True, owner_count, staff + ) error_msg += validator_utils.validate_location(json_data.get("location")) error_msg += validator_utils.validate_description(json_data.get("description"), staff) except Exception as validation_exception: # noqa: B902; eat all errors @@ -158,9 +154,7 @@ def validate_registration(json_data, staff: bool = False): return error_msg -def validate_transfer( # pylint: disable=too-many-branches - registration: MhrRegistration, json_data, staff: bool, group: str -): +def validate_transfer(registration: MhrRegistration, json_data, staff: bool, group: str): """Perform all transfer data validation checks not covered by schema validation.""" error_msg = "" try: @@ -171,50 +165,28 @@ def validate_transfer( # pylint: disable=too-many-branches error_msg += validator_utils.validate_doc_id(json_data, True) elif registration: error_msg += validator_utils.validate_ppr_lien(registration.mhr_number, MhrRegistrationTypes.TRANS, staff) - active_group_count: int = validator_utils.get_active_group_count(json_data, registration) + active_group_count: int = validator_owner_utils.get_active_group_count(json_data, registration) error_msg += validator_utils.validate_submitting_party(json_data) - error_msg += validator_utils.validate_owner_groups( + reg_staff: bool = registration.staff + registration.staff = staff + error_msg += validator_owner_utils.validate_owner_groups( json_data.get("addOwnerGroups"), False, registration, json_data.get("deleteOwnerGroups"), active_group_count ) - error_msg += validate_owner_party_type(json_data, json_data.get("addOwnerGroups"), False, active_group_count) + registration.staff = reg_staff + error_msg += validator_owner_utils.validate_owner_party_type( + json_data, json_data.get("addOwnerGroups"), False, active_group_count, staff + ) reg_type: str = json_data.get("registrationType", MhrRegistrationTypes.TRANS) error_msg += validator_utils.validate_registration_state(registration, staff, reg_type) error_msg += validator_utils.validate_draft_state(json_data) if registration and json_data.get("deleteOwnerGroups"): - error_msg += validator_utils.validate_delete_owners(registration, json_data) + error_msg += validator_owner_utils.validate_delete_owners(registration, json_data) if not staff: - if ( - not isinstance(json_data.get("declaredValue", 0), int) - or not json_data.get("declaredValue") - or json_data.get("declaredValue") < 0 - ): - error_msg += DECLARED_VALUE_REQUIRED - if reg_type == MhrRegistrationTypes.TRANS and ( - not json_data.get("transferDocumentType") - or json_data.get("transferDocumentType") - in ( - MhrDocumentTypes.TRANS_QUIT_CLAIM, - MhrDocumentTypes.TRANS_RECEIVERSHIP, - MhrDocumentTypes.TRANS_SEVER_GRANT, - ) - ): - if not json_data.get("consideration"): - error_msg += CONSIDERATION_REQUIRED - error_msg += validate_transfer_date(json_data) - if ( - json_data.get("deleteOwnerGroups") - and len(json_data.get("deleteOwnerGroups")) != 1 - and group == QUALIFIED_USER_GROUP - and len(json_data.get("deleteOwnerGroups")) != validator_utils.get_existing_group_count(registration) - ): - error_msg += TRAN_QUALIFIED_DELETE - error_msg += validate_transfer_dealer(registration, json_data, reg_type, group) + error_msg += validate_transfer_non_staff(registration, json_data, reg_type, group) if reg_type != MhrRegistrationTypes.TRANS and json_data.get("transferDocumentType"): error_msg += TRANS_DOC_TYPE_INVALID - elif not staff and json_data.get("transferDocumentType"): - error_msg += TRANS_DOC_TYPE_NOT_ALLOWED if reg_utils.is_transfer_due_to_death(json_data.get("registrationType")): - error_msg += validate_transfer_death(registration, json_data, group, active_group_count) + error_msg += validate_transfer_death(registration, json_data, staff, group, active_group_count) except Exception as validation_exception: # noqa: B902; eat all errors logger.error("validate_transfer exception: " + str(validation_exception)) error_msg += VALIDATOR_ERROR @@ -474,25 +446,38 @@ def validate_permit_location_address(json_data: dict, current_location: dict) -> return error_msg -def existing_owner_added(new_owners, owner) -> bool: - """Check if the existing owner name matches an owner name in the new group.""" - if owner and new_owners: - for owner_json in new_owners: - if ( - owner_json.get("individualName") - and owner.get("individualName") - and owner_json["individualName"].get("last") == owner["individualName"].get("last") - and owner_json["individualName"].get("first") == owner["individualName"].get("first") - ): - if owner_json["individualName"].get("middle", "") == owner["individualName"].get("middle", ""): - return True - elif ( - owner_json.get("organizationName") - and owner.get("organizationName") - and owner_json.get("organizationName") == owner.get("organizationName") - ): - return True - return False +def validate_transfer_non_staff(registration: MhrRegistration, json_data, reg_type: str, group: str): + """Perform non-registries staff transfer extra data validation checks.""" + error_msg = "" + if ( + not isinstance(json_data.get("declaredValue", 0), int) + or not json_data.get("declaredValue") + or json_data.get("declaredValue") < 0 + ): + error_msg += DECLARED_VALUE_REQUIRED + if reg_type == MhrRegistrationTypes.TRANS and ( + not json_data.get("transferDocumentType") + or json_data.get("transferDocumentType") + in ( + MhrDocumentTypes.TRANS_QUIT_CLAIM, + MhrDocumentTypes.TRANS_RECEIVERSHIP, + MhrDocumentTypes.TRANS_SEVER_GRANT, + ) + ): + if not json_data.get("consideration"): + error_msg += CONSIDERATION_REQUIRED + error_msg += validate_transfer_date(json_data) + if ( + json_data.get("deleteOwnerGroups") + and len(json_data.get("deleteOwnerGroups")) != 1 + and group == QUALIFIED_USER_GROUP + and len(json_data.get("deleteOwnerGroups")) != validator_owner_utils.get_existing_group_count(registration) + ): + error_msg += TRAN_QUALIFIED_DELETE + error_msg += validate_transfer_dealer(registration, json_data, reg_type, group) + if json_data.get("transferDocumentType"): + error_msg += TRANS_DOC_TYPE_NOT_ALLOWED + return error_msg def validate_transfer_death_existing_owners(reg_type: str, modified_group: dict, group: str): @@ -517,57 +502,40 @@ def validate_transfer_death_existing_owners(reg_type: str, modified_group: dict, return error_msg -def new_owner_exists(modified_group, owner) -> bool: - """Check if the new owner name matches an existing group owner name.""" - if owner and modified_group and modified_group.get("owners"): - for owner_json in modified_group.get("owners"): - if ( - owner_json.get("individualName") - and owner.get("individualName") - and owner_json["individualName"].get("last") == owner["individualName"].get("last") - and owner_json["individualName"].get("first") == owner["individualName"].get("first") - ): - if owner_json["individualName"].get("middle", "") == owner["individualName"].get("middle", ""): - return True - elif ( - owner_json.get("organizationName") - and owner.get("organizationName") - and owner_json.get("organizationName") == owner.get("organizationName") - ): - return True - return False - - def validate_transfer_death_new_owners(reg_type: str, new_owners, modified_group): """Apply new owner validation rules specific to transfer due to death registration types.""" error_msg: str = "" if not new_owners: return error_msg - exec_count: int = 0 + party_count: int = 0 for owner in new_owners: party_type = owner.get("partyType") - if ( - reg_type == MhrRegistrationTypes.TRAND - and party_type - and party_type not in (MhrPartyTypes.OWNER_BUS, MhrPartyTypes.OWNER_IND) - ): - error_msg += TRAN_DEATH_NEW_OWNER - elif reg_type == MhrRegistrationTypes.TRANS_ADMIN and ( - not party_type or party_type != MhrPartyTypes.ADMINISTRATOR + if reg_type == MhrRegistrationTypes.TRAND and ( + not party_type or party_type in (MhrPartyTypes.OWNER_BUS, MhrPartyTypes.OWNER_IND) ): - error_msg += TRAN_ADMIN_NEW_OWNER + party_count += 1 + elif reg_type == MhrRegistrationTypes.TRANS_ADMIN and party_type and party_type == MhrPartyTypes.ADMINISTRATOR: + party_count += 1 elif ( reg_type in (MhrRegistrationTypes.TRANS_WILL, MhrRegistrationTypes.TRANS_AFFIDAVIT) and party_type and party_type == MhrPartyTypes.EXECUTOR ): - exec_count += 1 - if reg_type == MhrRegistrationTypes.TRAND and modified_group and not new_owner_exists(modified_group, owner): + party_count += 1 + if ( + reg_type == MhrRegistrationTypes.TRAND + and modified_group + and not validator_owner_utils.new_owner_exists(modified_group, owner) + ): error_msg += TRAN_DEATH_ADD_OWNER - if exec_count != len(new_owners) and reg_type == MhrRegistrationTypes.TRANS_AFFIDAVIT: + if party_count < 1 and reg_type == MhrRegistrationTypes.TRANS_AFFIDAVIT: error_msg += TRAN_AFFIDAVIT_NEW_OWNER - elif exec_count != len(new_owners) and reg_type == MhrRegistrationTypes.TRANS_WILL: + elif party_count < 1 and reg_type == MhrRegistrationTypes.TRANS_WILL: error_msg += TRAN_WILL_NEW_OWNER + elif party_count < 1 and reg_type == MhrRegistrationTypes.TRANS_ADMIN: + error_msg += TRAN_ADMIN_NEW_OWNER + elif party_count < 1 and reg_type == MhrRegistrationTypes.TRAND: + error_msg += TRAN_DEATH_NEW_OWNER return error_msg @@ -605,25 +573,6 @@ def validate_transfer_cert_corp(reg_type: str, owner_json: dict) -> str: return error_msg -def is_delete_exec_admin(reg_type: str, owner_json: dict) -> bool: - """Evaluate if a deleted owner is an executor or an administrator for one of the death transfers types.""" - if reg_type == MhrRegistrationTypes.TRANS_WILL and owner_json.get("partyType", "") in ( - MhrPartyTypes.EXECUTOR, - MhrPartyTypes.ADMINISTRATOR, - MhrPartyTypes.TRUST, - MhrPartyTypes.TRUSTEE, - ): - return True - if reg_type == MhrRegistrationTypes.TRANS_ADMIN and owner_json.get("partyType", "") in ( - MhrPartyTypes.EXECUTOR, - MhrPartyTypes.ADMINISTRATOR, - MhrPartyTypes.TRUST, - MhrPartyTypes.TRUSTEE, - ): - return True - return False - - def validate_transfer_death_owners(reg_type: str, new_owners, delete_owners): """Apply owner delete/add validation rules specific to transfer due to death registration types.""" error_msg: str = "" @@ -631,14 +580,17 @@ def validate_transfer_death_owners(reg_type: str, new_owners, delete_owners): death_count: int = 0 party_count: int = 0 for owner_json in delete_owners: - if not existing_owner_added(new_owners, owner_json) and reg_type == MhrRegistrationTypes.TRAND: + if ( + not validator_owner_utils.existing_owner_added(new_owners, owner_json) + and reg_type == MhrRegistrationTypes.TRAND + ): error_msg += validate_transfer_cert_corp(reg_type, owner_json) elif reg_type in ( MhrRegistrationTypes.TRANS_WILL, MhrRegistrationTypes.TRANS_AFFIDAVIT, MhrRegistrationTypes.TRANS_ADMIN, ): - if is_delete_exec_admin(reg_type, owner_json): + if validator_owner_utils.is_delete_exec_admin(reg_type, owner_json): party_count += 1 elif ( not owner_json.get("deathCertificateNumber") @@ -663,32 +615,33 @@ def validate_transfer_death_owners(reg_type: str, new_owners, delete_owners): return error_msg -def validate_transfer_death(registration: MhrRegistration, json_data, group: str, active_group_count: int): +def validate_transfer_death( + registration: MhrRegistration, json_data: dict, staff: bool, group: str, active_group_count: int +): """Apply validation rules specific to transfer due to death registration types.""" error_msg: str = "" if not json_data.get("deleteOwnerGroups") or not json_data.get("addOwnerGroups"): return error_msg reg_type: str = json_data.get("registrationType") - tenancy_type: str = None - modified_group: dict = None - if json_data.get("deleteOwnerGroups"): - modified_group = validator_utils.get_modified_group( - registration, json_data["deleteOwnerGroups"][0].get("groupId", 0) - ) - if len(json_data.get("deleteOwnerGroups")) != 1 or len(json_data.get("addOwnerGroups")) != 1: - error_msg += TRAN_DEATH_GROUP_COUNT - if json_data["deleteOwnerGroups"][0].get("type"): - tenancy_type = json_data["deleteOwnerGroups"][0].get("type") - if reg_type == MhrRegistrationTypes.TRAND and tenancy_type != MhrTenancyTypes.JOINT: + # Exactly one group deleted ignoring owner edits. + delete_group, modified_group = validator_owner_utils.get_death_delete_group(registration, json_data) + # Do not count groups where the owner information is updated. + # Exactly 1 group added ignoring owner edits. + new_group = validator_owner_utils.get_death_add_group(json_data) + error_msg += validator_owner_utils.validate_death_group_counts(registration, json_data, delete_group, new_group) + if not new_group or not delete_group: + return error_msg + if delete_group.get("type"): + if reg_type == MhrRegistrationTypes.TRAND and delete_group.get("type") != MhrTenancyTypes.JOINT: error_msg += TRAN_DEATH_JOINT_TYPE - new_owners = json_data["addOwnerGroups"][0].get("owners") + new_owners = new_group.get("owners") # check existing owners. - error_msg += validate_transfer_death_existing_owners(reg_type, modified_group, group) + if not staff: + error_msg += validate_transfer_death_existing_owners(reg_type, modified_group, group) # check new owners. error_msg += validate_transfer_death_new_owners(reg_type, new_owners, modified_group) - delete_owners = json_data["deleteOwnerGroups"][0].get("owners") - if new_owners and delete_owners: - error_msg += validate_transfer_death_owners(reg_type, new_owners, delete_owners) + if new_owners and delete_group.get("owners"): + error_msg += validate_transfer_death_owners(reg_type, new_owners, delete_group.get("owners")) if ( reg_type == MhrRegistrationTypes.TRANS_AFFIDAVIT and json_data.get("declaredValue") @@ -708,16 +661,6 @@ def validate_transfer_death(registration: MhrRegistration, json_data, group: str return error_msg -def delete_group(group_id: int, delete_groups): - """Check if owner group is flagged for deletion.""" - if not delete_groups or group_id < 1: - return False - for group in delete_groups: - if group.get("groupId", 0) == group_id: - return True - return False - - def validate_manufacturer_permit(mhr_number: str, json_data: dict, current_location: dict) -> str: """Validate transport permit business rules specific to manufacturers.""" error_msg = "" @@ -772,107 +715,6 @@ def location_address_match(current_location, request_location): return False -def validate_owner_party_type( # pylint: disable=too-many-branches - json_data, groups, new: bool, active_group_count: int -): - """Verify owner groups are valid.""" - error_msg = "" - owner_death: bool = reg_utils.is_transfer_due_to_death_staff(json_data.get("registrationType")) - if not groups: - return error_msg - for group in groups: - if not new and len(groups) > 1 and group_owners_unchanged(json_data, group): - continue - party_count: int = 0 - owner_count: int = 0 - group_parties_invalid: bool = False - first_party_type: str = None - if group.get("owners"): - owner_count = len(group.get("owners")) - for owner in group["owners"]: - party_type = owner.get("partyType", None) - if party_type and party_type in ( - MhrPartyTypes.ADMINISTRATOR, - MhrPartyTypes.EXECUTOR, - MhrPartyTypes.TRUSTEE, - ): - party_count += 1 - if not first_party_type: - first_party_type = party_type - if first_party_type and party_type != first_party_type: - group_parties_invalid = True - if ( - party_type - and not owner.get("description") - and party_type - in (MhrPartyTypes.ADMINISTRATOR, MhrPartyTypes.EXECUTOR, MhrPartyTypes.TRUST, MhrPartyTypes.TRUSTEE) - ): - error_msg += OWNER_DESCRIPTION_REQUIRED - if ( - not new - and not owner_death - and not json_data.get("transferDocumentType") - and party_type - and party_type - in (MhrPartyTypes.ADMINISTRATOR, MhrPartyTypes.EXECUTOR, MhrPartyTypes.TRUST, MhrPartyTypes.TRUSTEE) - ): - error_msg += TRANSFER_PARTY_TYPE_INVALID - if active_group_count < 2 and group.get("type", "") == MhrTenancyTypes.NA and owner_count == 1: - error_msg += TENANCY_TYPE_NA_INVALID # SOLE owner cannot be NA - elif active_group_count > 1 and party_count > 0 and group.get("type", "") != MhrTenancyTypes.NA: - error_msg += TENANCY_PARTY_TYPE_INVALID # COMMON scenario - elif ( - active_group_count == 1 - and owner_count > 1 - and party_count > 0 - and group.get("type", "") != MhrTenancyTypes.NA - ): - error_msg += TENANCY_PARTY_TYPE_INVALID # JOINT scenario - if new and group_parties_invalid: - error_msg += GROUP_PARTY_TYPE_INVALID - return error_msg - - -def group_owners_unchanged(json_data, add_group) -> bool: - """Check if the owners in an added group are identical to the owners in a deleted group.""" - if not json_data.get("deleteOwnerGroups") or not add_group.get("owners"): - return False - for group in json_data.get("deleteOwnerGroups"): - if group.get("owners") and len(group["owners"]) == len(add_group["owners"]): - identical: bool = True - for add_owner in add_group.get("owners"): - owner_match: bool = False - for del_owner in group.get("owners"): - if owner_name_address_match(add_owner, del_owner): - owner_match = True - if not owner_match: - identical = False - if identical: - return True - return False - - -def owner_name_address_match(owner1, owner2) -> bool: - """Check if 2 owner json name and addresses are identical.""" - address_match: bool = False - name_match: bool = False - if owner1.get("address") and owner2.get("address") and owner1.get("address") == owner2.get("address"): - address_match = True - if ( - owner1.get("organizationName") - and owner2.get("organizationName") - and owner1.get("organizationName") == owner2.get("organizationName") - ): - name_match = True - elif ( - owner1.get("individualName") - and owner2.get("individualName") - and owner1.get("individualName") == owner2.get("individualName") - ): - name_match = True - return address_match and name_match - - def validate_active_permit(registration: MhrRegistration, account_id: str) -> str: """Non-staff verify an active transport permit was created by the same account.""" error_msg = "" diff --git a/mhr-api/src/mhr_api/utils/validator_owner_utils.py b/mhr-api/src/mhr_api/utils/validator_owner_utils.py new file mode 100644 index 000000000..1571cdb89 --- /dev/null +++ b/mhr-api/src/mhr_api/utils/validator_owner_utils.py @@ -0,0 +1,711 @@ +# Copyright © 2019 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. +"""This module holds owner group and owner validation functions. + +Refactored from validator_utils. +""" +from mhr_api.models import MhrOwnerGroup, MhrParty, MhrRegistration +from mhr_api.models import registration_utils as reg_utils +from mhr_api.models.registration_json_utils import is_identical_owner_name +from mhr_api.models.type_tables import MhrOwnerStatusTypes, MhrPartyTypes, MhrRegistrationTypes, MhrTenancyTypes +from mhr_api.utils.logging import logger + +from .validator_utils import validate_individual_name, validate_text + +DELETE_GROUP_ID_INVALID = "The owner group with ID {group_id} is not active and cannot be changed. " +DELETE_GROUP_ID_NONEXISTENT = "No owner group with ID {group_id} exists. " +DELETE_GROUP_TYPE_INVALID = "The owner group tenancy type with ID {group_id} is invalid. " +GROUP_INTEREST_MISMATCH = "The owner group interest numerator sum does not equal the interest common denominator. " +GROUP_NUMERATOR_MISSING = "The owner group interest numerator is required and must be an integer greater than 0. " +GROUP_DENOMINATOR_MISSING = "The owner group interest denominator is required and must be an integer greater than 0. " +TENANCY_TYPE_NA_INVALID = "Tenancy type NA is not allowed when there is 1 active owner group with 1 owner. " +TENANCY_TYPE_NA_INVALID2 = ( + "Tenancy type NA is only allowed when all owners are ADMINISTRATOR, EXECUTOR, or TRUSTEE party types. " +) +OWNERS_JOINT_INVALID = "The owner group must contain at least 2 owners. " +OWNERS_COMMON_INVALID = "Each COMMON owner group must contain exactly 1 owner. " +OWNERS_COMMON_SOLE_INVALID = ( + "SOLE owner group tenancy type is not allowed when there is more than 1 " + "owner group. Use COMMON instead. " +) +GROUP_COMMON_INVALID = "More than 1 group is required with the Tenants in Common owner group type. " +ADD_SOLE_OWNER_INVALID = "Only one sole owner and only one sole owner group can be added. " +OWNER_DESCRIPTION_REQUIRED = "Owner description is required for the owner party type. " +TRANSFER_PARTY_TYPE_INVALID = "Owner party type of administrator, executor, trustee not allowed for this registration. " +TENANCY_PARTY_TYPE_INVALID = "Owner group tenancy type must be NA for executors, trustees, or administrators. " +GROUP_PARTY_TYPE_INVALID = ( + "For TRUSTEE, ADMINISTRATOR, or EXECUTOR, all owner party types within the group " + "must be identical. " +) +TRAN_DEATH_GROUP_COUNT = "Only one owner group can be modified in a transfer due to death registration. " +TRAN_DEATH_DELETE_MISSING = "Death transfer excluding owner edits no request deleteOwnerGroups group found. " +TRAN_DEATH_ADD_MISSING = "Death transfer excluding owner edits no request addOwnerGroups group found. " + + +def get_existing_group_count(registration: MhrRegistration) -> int: + """Count number of existing owner groups.""" + group_count: int = 0 + if not registration: + return group_count + for existing in registration.owner_groups: + if existing.status_type in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): + group_count += 1 + if registration.change_registrations: + for reg in registration.change_registrations: + if reg.owner_groups: + for existing in reg.owner_groups: + if existing.status_type in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): + group_count += 1 + return group_count + + +def owner_name_match(registration: MhrRegistration = None, request_owner=None): # pylint: disable=too-many-branches + """Verify the request owner name matches one of the current owner names.""" + if not registration or not request_owner: + return False + request_name: str = "" + first_name: str = "" + last_name: str = "" + match: bool = False + is_business = request_owner.get("organizationName") + if is_business: + request_name = request_owner.get("organizationName").strip().upper() + elif ( + request_owner.get("individualName") + and request_owner["individualName"].get("first") + and request_owner["individualName"].get("last") + ): + first_name = request_owner["individualName"].get("first").strip().upper() + last_name = request_owner["individualName"].get("last").strip().upper() + if not request_name and not last_name: + return False + if registration.owner_groups: + for group in registration.owner_groups: + if group.status_type == MhrOwnerStatusTypes.ACTIVE: + for owner in group.owners: + if is_business and owner.business_name == request_name: + match = True + elif not is_business and owner.first_name == first_name and owner.last_name == last_name: + match = True + if not match and registration.change_registrations: # pylint: disable=too-many-nested-blocks + for reg in registration.change_registrations: + for group in reg.owner_groups: + if group.status_type == MhrOwnerStatusTypes.ACTIVE: + for owner in group.owners: + if is_business and owner.business_name == request_name: + match = True + elif not is_business and owner.first_name == first_name and owner.last_name == last_name: + match = True + return match + + +def get_existing_owner_groups(registration: MhrRegistration) -> dict: + """Get the existing active/exempt owner groups.""" + groups = [] + for existing in registration.owner_groups: + if existing.status_type in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): + groups.append(existing.json) + if registration.change_registrations: + for reg in registration.change_registrations: + if reg.owner_groups: + for existing in reg.owner_groups: + if existing.status_type in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): + groups.append(existing.json) + return groups + + +def is_valid_dealer_transfer_owner(registration: MhrRegistration, qs: dict) -> bool: + """Check qs dealer name matches owner name and owner is a sole owner.""" + qs_name: str = str(qs.get("businessName", "")).strip().upper() + dba_name: str = qs.get("dbaName", "") + if dba_name: + dba_name = dba_name.strip().upper() + logger.debug(f"is_valid_dealer_transfer_owner checking dealer name={qs_name} dba_name={dba_name}") + groups = get_existing_owner_groups(registration) + if not groups or len(groups) > 1: + return False + owners = groups[0].get("owners") + if owners and len(owners) == 1: + owner_name: str = owners[0].get("organizationName", "") + if owner_name: + logger.debug(f"Comparing owner name={owner_name} with qs name and dba name") + owner_name = owner_name.strip().upper() + if owner_name == qs_name or (dba_name and owner_name == dba_name): + return True + return False + + +def validate_delete_owners( # pylint: disable=too-many-branches + registration: MhrRegistration = None, json_data: dict = None +) -> str: + """Check groups id's and owners are valid for deleted groups.""" + error_msg = "" + if not registration or not json_data.get("deleteOwnerGroups"): + return error_msg + for deleted in json_data["deleteOwnerGroups"]: # pylint: disable=too-many-nested-blocks + if deleted.get("groupId"): + deleted_group = None + group_id = deleted["groupId"] + for existing in registration.owner_groups: + if existing.group_id == group_id: + deleted_group = existing + if not deleted_group and registration.change_registrations: + for reg in registration.change_registrations: + if reg.owner_groups: + for existing in reg.owner_groups: + if existing.group_id == group_id: + deleted_group = existing + if deleted_group: + tenancy_type = deleted.get("type") + # Data migration legacy owner group can have a status of EXEMPT. + if deleted_group.status_type not in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): + error_msg += DELETE_GROUP_ID_INVALID.format(group_id=group_id) + if tenancy_type and deleted_group.tenancy_type != tenancy_type and tenancy_type != MhrTenancyTypes.NA: + error_msg += DELETE_GROUP_TYPE_INVALID.format(group_id=group_id) + else: + error_msg += DELETE_GROUP_ID_NONEXISTENT.format(group_id=group_id) + return error_msg + + +def delete_group(group_id: int, delete_groups): + """Check if owner group is flagged for deletion.""" + if not delete_groups or group_id < 1: + return False + for group in delete_groups: + if group.get("groupId", 0) == group_id: + return True + return False + + +def interest_required(groups, registration: MhrRegistration = None, delete_groups=None) -> bool: + """Determine if group interest is required.""" + group_count: int = len(groups) # Verify interest if multiple groups or existing interest. + if group_count > 1: + return True + if not registration: + return False + for existing in registration.owner_groups: + if ( + existing.status_type == MhrOwnerStatusTypes.ACTIVE + and existing.tenancy_type != MhrTenancyTypes.SOLE + and not delete_group(existing.group_id, delete_groups) + and existing.interest_denominator > 0 + ): + group_count += 1 + if registration.change_registrations: + for reg in registration.change_registrations: + if reg.owner_groups: + for existing in reg.owner_groups: + if ( + existing.status_type == MhrOwnerStatusTypes.ACTIVE + and existing.tenancy_type != MhrTenancyTypes.SOLE + and not delete_group(existing.group_id, delete_groups) + and existing.interest_denominator > 0 + ): + group_count += 1 + return group_count > 1 + + +def validate_group_interest( # pylint: disable=too-many-branches + groups, + denominator: int, + registration: MhrRegistration = None, + delete_groups=None, +): + """Verify owner group interest values are valid.""" + error_msg = "" + numerator_sum: int = 0 + group_count: int = len(groups) # Verify interest if multiple groups or existing interest. + if registration: # pylint: disable=too-many-nested-blocks + for existing in registration.owner_groups: + if ( + existing.status_type == MhrOwnerStatusTypes.ACTIVE + and existing.tenancy_type != MhrTenancyTypes.SOLE + and not delete_group(existing.group_id, delete_groups) + ): + den = existing.interest_denominator + if den > 0: + group_count += 1 + if den == denominator: + numerator_sum += existing.interest_numerator + elif den < denominator: + numerator_sum += denominator / den * existing.interest_numerator + else: + numerator_sum += int((denominator * existing.interest_numerator) / den) + if registration.change_registrations: + for reg in registration.change_registrations: + if reg.owner_groups: + for existing in reg.owner_groups: + if ( + existing.status_type == MhrOwnerStatusTypes.ACTIVE + and existing.tenancy_type != MhrTenancyTypes.SOLE + and not delete_group(existing.group_id, delete_groups) + ): + den = existing.interest_denominator + if den > 0: + group_count += 1 + if den == denominator: + numerator_sum += existing.interest_numerator + elif den < denominator: + numerator_sum += denominator / den * existing.interest_numerator + else: + numerator_sum += int((denominator * existing.interest_numerator) / den) + logger.debug(f"group_count={group_count} denominator={denominator}") + if group_count < 2: # Could have transfer of joint tenants with no interest. + return error_msg + for group in groups: + num = group.get("interestNumerator", 0) + den = group.get("interestDenominator", 0) + if num and den and num > 0 and den > 0: + if den == denominator: + numerator_sum += num + else: + numerator_sum += denominator / den * num + if numerator_sum != denominator: + error_msg = GROUP_INTEREST_MISMATCH + return error_msg + + +def get_modified_group(registration: MhrRegistration, group_id: int) -> dict: + """Find the existing owner group as JSON, matching on the group id.""" + group = {} + if not registration: + return group + for existing in registration.owner_groups: + if existing.group_id == group_id: + group = existing.json + break + if not group and registration.change_registrations: + for reg in registration.change_registrations: + if reg.owner_groups: + for existing in reg.owner_groups: + if existing.group_id == group_id: + group = existing.json + break + return group + + +def validate_owner_group(group, int_required: bool = False, staff: bool = False): + """Verify owner group is valid.""" + error_msg = "" + if not group: + return error_msg + tenancy_type: str = group.get("type", "") + if tenancy_type == MhrTenancyTypes.COMMON or int_required: + if not group.get("interestNumerator") or group.get("interestNumerator", 0) < 1: + error_msg += GROUP_NUMERATOR_MISSING + if not group.get("interestDenominator") or group.get("interestDenominator", 0) < 1: + error_msg += GROUP_DENOMINATOR_MISSING + if tenancy_type == MhrTenancyTypes.NA and group.get("owners") and len(group.get("owners")) > 1: + owner_count: int = 0 + for owner in group.get("owners"): + if not owner.get("partyType") or owner.get("partyType") in ( + MhrPartyTypes.OWNER_BUS, + MhrPartyTypes.OWNER_IND, + ): + owner_count += 1 + if owner_count != 0 and not staff: + error_msg += TENANCY_TYPE_NA_INVALID2 + if tenancy_type == MhrTenancyTypes.JOINT and (not group.get("owners") or len(group.get("owners")) < 2): + error_msg += OWNERS_JOINT_INVALID + elif tenancy_type == MhrTenancyTypes.COMMON and (not group.get("owners") or len(group.get("owners")) > 1): + error_msg += OWNERS_COMMON_INVALID + elif tenancy_type == MhrTenancyTypes.SOLE and int_required: + error_msg += OWNERS_COMMON_SOLE_INVALID + return error_msg + + +def validate_owner(owner: dict) -> str: + """Verify owner names are valid and legacy suffix length is valid.""" + error_msg = "" + if not owner: + return error_msg + desc: str = "owner" + if owner.get("organizationName"): + error_msg += validate_text(owner.get("organizationName"), desc + " organization name") + elif owner.get("individualName"): + error_msg += validate_individual_name(owner.get("individualName"), desc) + return error_msg + + +def common_tenancy(groups, new: bool, active_count: int = 0) -> bool: + """Determine if the owner groups is a tenants in common scenario.""" + if new and groups and len(groups) == 1: + return False + for group in groups: + group_type = group.get("type", "") + if group_type and group_type != MhrTenancyTypes.SOLE and active_count > 1: + return True + return False + + +def validate_owner_groups_common(groups, registration: MhrRegistration = None, delete_groups=None): + """Verify tenants in common owner groups are valid.""" + error_msg = "" + tc_owner_count_invalid: bool = False + staff: bool = False + if registration: + staff = registration.staff + common_denominator: int = 0 + int_required: bool = interest_required(groups, registration, delete_groups) + for group in groups: + if common_denominator == 0: + common_denominator = group.get("interestDenominator", 0) + elif group.get("interestDenominator", 0) > common_denominator: + common_denominator = group.get("interestDenominator", 0) + if not group.get("owners"): + tc_owner_count_invalid = True + error_msg += validate_owner_group(group, int_required, staff) + for owner in group.get("owners"): + error_msg += validate_owner(owner) + error_msg += validate_group_interest(groups, common_denominator, registration, delete_groups) + if tc_owner_count_invalid: + error_msg += OWNERS_COMMON_INVALID + return error_msg + + +def validate_owner_groups( + groups, new: bool, registration: MhrRegistration = None, delete_groups=None, active_count: int = 0 +): + """Verify owner groups are valid.""" + error_msg = "" + if not groups: + return error_msg + so_count: int = 0 + staff: bool = False + if registration: + staff = registration.staff + if common_tenancy(groups, new, active_count): + return validate_owner_groups_common(groups, registration, delete_groups) + for group in groups: + tenancy_type: str = group.get("type", "") + if new and tenancy_type == MhrTenancyTypes.COMMON: + error_msg += GROUP_COMMON_INVALID + error_msg += validate_owner_group(group, False, staff) + for owner in group.get("owners"): + if tenancy_type == MhrTenancyTypes.SOLE: + so_count += 1 + error_msg += validate_owner(owner) + if so_count > 1 or (so_count == 1 and len(groups) > 1): + error_msg += ADD_SOLE_OWNER_INVALID + if not new and active_count == 1 and tenancy_type == MhrTenancyTypes.COMMON: + error_msg += GROUP_COMMON_INVALID + return error_msg + + +def get_active_group_count(json_data, registration: MhrRegistration) -> int: + """Count number of active owner groups.""" + group_count: int = 0 + if json_data.get("ownerGroups"): + group_count += len(json_data.get("ownerGroups")) + else: + if json_data.get("addOwnerGroups"): + group_count += len(json_data.get("addOwnerGroups")) + if json_data.get("deleteOwnerGroups"): + group_count -= len(json_data.get("deleteOwnerGroups")) + group_count += get_existing_group_count(registration) + return group_count + + +def owner_name_address_match(owner1, owner2) -> bool: + """Check if 2 owner json name and addresses are identical.""" + address_match: bool = False + name_match: bool = False + if owner1.get("address") and owner2.get("address") and owner1.get("address") == owner2.get("address"): + address_match = True + if ( + owner1.get("organizationName") + and owner2.get("organizationName") + and owner1.get("organizationName") == owner2.get("organizationName") + ): + name_match = True + elif ( + owner1.get("individualName") + and owner2.get("individualName") + and owner1.get("individualName") == owner2.get("individualName") + ): + name_match = True + return address_match and name_match + + +def group_owners_unchanged(json_data, add_group) -> bool: + """Check if the owners in an added group are identical to the owners in a deleted group.""" + if not json_data.get("deleteOwnerGroups") or not add_group.get("owners"): + return False + for group in json_data.get("deleteOwnerGroups"): + if group.get("owners") and len(group["owners"]) == len(add_group["owners"]): + identical: bool = True + for add_owner in add_group.get("owners"): + owner_match: bool = False + for del_owner in group.get("owners"): + if owner_name_address_match(add_owner, del_owner): + owner_match = True + if not owner_match: + identical = False + if identical: + return True + return False + + +def validate_owner_party_type( # pylint: disable=too-many-branches + json_data: dict, groups, new: bool, active_group_count: int, staff: bool +): + """Verify owner groups are valid.""" + error_msg = "" + owner_death: bool = reg_utils.is_transfer_due_to_death_staff(json_data.get("registrationType")) + if not groups: + return error_msg + for group in groups: + if not new and len(groups) > 1 and group_owners_unchanged(json_data, group): + continue + party_count: int = 0 + owner_count: int = 0 + group_parties_invalid: bool = False + first_party_type: str = None + if group.get("owners"): + owner_count = len(group.get("owners")) + for owner in group["owners"]: + party_type = owner.get("partyType", None) + if party_type and party_type in ( + MhrPartyTypes.ADMINISTRATOR, + MhrPartyTypes.EXECUTOR, + MhrPartyTypes.TRUSTEE, + ): + party_count += 1 + if not first_party_type: + first_party_type = party_type + if first_party_type and party_type != first_party_type: + group_parties_invalid = True + if ( + party_type + and not owner.get("description") + and party_type + in (MhrPartyTypes.ADMINISTRATOR, MhrPartyTypes.EXECUTOR, MhrPartyTypes.TRUST, MhrPartyTypes.TRUSTEE) + ): + error_msg += OWNER_DESCRIPTION_REQUIRED + if ( + not new + and not owner_death + and not json_data.get("transferDocumentType") + and party_type + and party_type + in (MhrPartyTypes.ADMINISTRATOR, MhrPartyTypes.EXECUTOR, MhrPartyTypes.TRUST, MhrPartyTypes.TRUSTEE) + ): + if not staff: + error_msg += TRANSFER_PARTY_TYPE_INVALID + if active_group_count < 2 and group.get("type", "") == MhrTenancyTypes.NA and owner_count == 1: + error_msg += TENANCY_TYPE_NA_INVALID # SOLE owner cannot be NA + elif active_group_count > 1 and party_count > 0 and group.get("type", "") != MhrTenancyTypes.NA: + error_msg += TENANCY_PARTY_TYPE_INVALID # COMMON scenario + elif ( + active_group_count == 1 + and owner_count > 1 + and party_count > 0 + and group.get("type", "") != MhrTenancyTypes.NA + ): + error_msg += TENANCY_PARTY_TYPE_INVALID # JOINT scenario + if new and group_parties_invalid: + error_msg += GROUP_PARTY_TYPE_INVALID + return error_msg + + +def is_delete_exec_admin(reg_type: str, owner_json: dict) -> bool: + """Evaluate if a deleted owner is an executor or an administrator for one of the death transfers types.""" + if reg_type == MhrRegistrationTypes.TRANS_WILL and owner_json.get("partyType", "") in ( + MhrPartyTypes.EXECUTOR, + MhrPartyTypes.ADMINISTRATOR, + MhrPartyTypes.TRUST, + MhrPartyTypes.TRUSTEE, + ): + return True + if reg_type == MhrRegistrationTypes.TRANS_ADMIN and owner_json.get("partyType", "") in ( + MhrPartyTypes.EXECUTOR, + MhrPartyTypes.ADMINISTRATOR, + MhrPartyTypes.TRUST, + MhrPartyTypes.TRUSTEE, + ): + return True + return False + + +def new_owner_exists(modified_group, owner) -> bool: + """Check if the new owner name matches an existing group owner name.""" + if owner and modified_group and modified_group.get("owners"): + for owner_json in modified_group.get("owners"): + if ( + owner_json.get("individualName") + and owner.get("individualName") + and owner_json["individualName"].get("last") == owner["individualName"].get("last") + and owner_json["individualName"].get("first") == owner["individualName"].get("first") + ): + if owner_json["individualName"].get("middle", "") == owner["individualName"].get("middle", ""): + return True + elif ( + owner_json.get("organizationName") + and owner.get("organizationName") + and owner_json.get("organizationName") == owner.get("organizationName") + ): + return True + return False + + +def existing_owner_added(new_owners, owner) -> bool: + """Check if the existing owner name matches an owner name in the new group.""" + if owner and new_owners: + for owner_json in new_owners: + if ( + owner_json.get("individualName") + and owner.get("individualName") + and owner_json["individualName"].get("last") == owner["individualName"].get("last") + and owner_json["individualName"].get("first") == owner["individualName"].get("first") + ): + if owner_json["individualName"].get("middle", "") == owner["individualName"].get("middle", ""): + return True + elif ( + owner_json.get("organizationName") + and owner.get("organizationName") + and owner_json.get("organizationName") == owner.get("organizationName") + ): + return True + return False + + +def match_group_owner(group: MhrOwnerGroup, owner_id: int) -> MhrParty: + """Find owner matching the owner id.""" + if group.status_type in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): + for owner in group.owners: + logger.info(f"match_group_owner group id={group.id} owner id={owner.id}") + if owner.id == owner_id: + return owner + return None + + +def is_deleted_owner_match(registration: MhrRegistration, request_owner) -> bool: + """For owner edits try and find the existing owner that matches by id and name.""" + if not registration or not request_owner or not request_owner.get("previousOwnerId"): + return False + owner_id: int = request_owner.get("previousOwnerId") + deleted_owner: MhrParty = None + logger.info(f"is_deleted_owner_match owner_id={owner_id}") + for group in registration.owner_groups: + deleted_owner = match_group_owner(group, owner_id) + if deleted_owner: + break + if not deleted_owner and registration.change_registrations: + for reg in registration.change_registrations: + for group in reg.owner_groups: + if not deleted_owner: + deleted_owner = match_group_owner(group, owner_id) + else: + break + if not deleted_owner: + return False + deleted_json = deleted_owner.json + if is_identical_owner_name(deleted_json, request_owner): + logger.info(f"owner_name_match id match on deleted owner={deleted_json}") + return True + return False + + +def is_edit_group(registration: MhrRegistration, add_group: dict) -> bool: + """Look for a valid edit owner in the added group.""" + edit_owner: dict = None + for owner in add_group.get("owners"): + if owner.get("previousOwnerId"): + edit_owner = owner + break + if not edit_owner: + return False + return is_deleted_owner_match(registration, edit_owner) + + +def is_edit_owner_id(add_groups, owner_id: int) -> bool: + """Look for new owner group with the matching deleted owner ID.""" + if not add_groups or not owner_id or owner_id < 0: + return False + for group in add_groups: + for owner in group.get("owners"): + if owner.get("previousOwnerId", 0) == owner_id: + return True + return False + + +def get_death_group_count(registration: MhrRegistration, json_data: dict) -> int: + """Transfer death registration get group changed count ignoring owner edits.""" + del_count: int = len(json_data.get("deleteOwnerGroups")) + add_count: int = len(json_data.get("addOwnerGroups")) + if del_count == 1 and add_count == 1: + return 1 + # Either no owner edits or number of deleted and added groups with edits should match + if del_count != add_count: + if del_count < add_count: + return add_count + return del_count + # Ignore groups with valid owner edits. + edit_count: int = 0 + for group in json_data.get("addOwnerGroups"): + if is_edit_group(registration, group): + edit_count += 1 + if edit_count == 0: + return add_count + if add_count - edit_count == 0: + return 1 + return add_count - edit_count + + +def get_death_add_group(json_data: dict) -> dict: + """Transfer death registration get single added group ignoring owner edits.""" + if len(json_data.get("addOwnerGroups")) == 1: + return json_data["addOwnerGroups"][0] + for group in json_data.get("addOwnerGroups"): + edit_group: bool = False + for owner in group.get("owners"): + if owner.get("previousOwnerId"): + edit_group = True + break + if not edit_group: + return group + return None + + +def get_death_delete_group(registration: MhrRegistration, json_data: dict): + """Transfer death registration get single added group ignoring owner edits.""" + del_group: dict = None + modified_group: dict = None + if len(json_data.get("deleteOwnerGroups")) == 1: + del_group = json_data["deleteOwnerGroups"][0] + elif json_data.get("addOwnerGroups"): + # Look for a deleted owner group with no corresponding add owner group edit Ids. + for group in json_data.get("deleteOwnerGroups"): + edit_group: bool = False + for owner in group.get("owners"): + if is_edit_owner_id(json_data.get("addOwnerGroups"), owner.get("ownerId")): + edit_group = True + break + if not edit_group: + del_group = group + break + if del_group: + modified_group = get_modified_group(registration, del_group.get("groupId", 0)) + return del_group, modified_group + + +def validate_death_group_counts(registration: MhrRegistration, json_data: dict, del_group, new_group) -> str: + """Transfer death registration check add/delete group counts ignoring edits.""" + error_msg: str = "" + if get_death_group_count(registration, json_data) != 1: + error_msg += TRAN_DEATH_GROUP_COUNT + if not del_group: + error_msg += TRAN_DEATH_DELETE_MISSING + if not new_group: + error_msg += TRAN_DEATH_ADD_MISSING + return error_msg diff --git a/mhr-api/src/mhr_api/utils/validator_utils.py b/mhr-api/src/mhr_api/utils/validator_utils.py index b232aeccb..2ef4b59aa 100644 --- a/mhr-api/src/mhr_api/utils/validator_utils.py +++ b/mhr-api/src/mhr_api/utils/validator_utils.py @@ -15,7 +15,6 @@ Refactored from registration_validator. """ -# pylint: disable=too-many-lines import copy from mhr_api.models import MhrDraft, MhrRegistration, registration_json_utils @@ -25,12 +24,9 @@ MhrDocumentTypes, MhrLocationTypes, MhrNoteStatusTypes, - MhrOwnerStatusTypes, - MhrPartyTypes, MhrRegistrationStatusTypes, MhrRegistrationTypes, MhrStatusTypes, - MhrTenancyTypes, ) from mhr_api.services import ltsa from mhr_api.utils import valid_charset @@ -69,10 +65,6 @@ EXEMPT_EXNR_INVALID = "Registration not allowed: the home is exempt because of an existing non-residential exemption. " EXEMPT_EXRS_INVALID = "Residential exemption registration not allowed: the home is already exempt. " EXEMPT_PERMIT_INVALID = "Registration not allowed: the home is not exempt because of a transport permit location. " -DELETE_GROUP_ID_INVALID = "The owner group with ID {group_id} is not active and cannot be changed. " -DELETE_GROUP_ID_NONEXISTENT = "No owner group with ID {group_id} exists. " -DELETE_GROUP_TYPE_INVALID = "The owner group tenancy type with ID {group_id} is invalid. " -GROUP_INTEREST_MISMATCH = "The owner group interest numerator sum does not equal the interest common denominator. " MHR_NUMBER_INVALID = "MHR number {mhr_num} either is greater than the existng maximum MHR number or already exists. " LOCATION_INVALID_IDENTICAL = "The new location cannot be identical to the existing location. " LOCATION_DEALER_REQUIRED = "Location dealer/manufacturer name is required for this registration. " @@ -108,18 +100,6 @@ ) LOCATION_TAX_CERT_REQUIRED = "Location tax certificate and tax certificate expiry date are required. " STATUS_CONFIRMATION_REQUIRED = "The land status confirmation is required for this registration. " -GROUP_NUMERATOR_MISSING = "The owner group interest numerator is required and must be an integer greater than 0. " -GROUP_DENOMINATOR_MISSING = "The owner group interest denominator is required and must be an integer greater than 0. " -TENANCY_TYPE_NA_INVALID2 = ( - "Tenancy type NA is only allowed when all owners are ADMINISTRATOR, EXECUTOR, " + "or TRUSTEE party types. " -) -OWNERS_JOINT_INVALID = "The owner group must contain at least 2 owners. " -OWNERS_COMMON_INVALID = "Each COMMON owner group must contain exactly 1 owner. " -OWNERS_COMMON_SOLE_INVALID = ( - "SOLE owner group tenancy type is not allowed when there is more than 1 " + "owner group. Use COMMON instead. " -) -GROUP_COMMON_INVALID = "More than 1 group is required with the Tenants in Common owner group type. " -ADD_SOLE_OWNER_INVALID = "Only one sole owner and only one sole owner group can be added. " CANCEL_PERMIT_INVALID = "Cancel Transport Permit not allowed: no active, non-expired transport permit exists. " PPR_REG_TYPE_ALL = " SA_TAX TA_TAX TM_TAX " PPR_REG_TYPE_GOV = " SA_GOV TA_GOV TM_GOV " @@ -402,23 +382,6 @@ def validate_pid(pid: str): return error_msg -def get_existing_group_count(registration: MhrRegistration) -> int: - """Count number of existing owner groups.""" - group_count: int = 0 - if not registration: - return group_count - for existing in registration.owner_groups: - if existing.status_type in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): - group_count += 1 - if registration.change_registrations: - for reg in registration.change_registrations: - if reg.owner_groups: - for existing in reg.owner_groups: - if existing.status_type in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): - group_count += 1 - return group_count - - def check_state_note( registration: MhrRegistration, staff: bool, error_msg: str, reg_type: str, doc_type: str = None ) -> str: @@ -526,232 +489,6 @@ def validate_description(description, staff: bool): return error_msg -def owner_name_match(registration: MhrRegistration = None, request_owner=None): # pylint: disable=too-many-branches - """Verify the request owner name matches one of the current owner names.""" - if not registration or not request_owner: - return False - request_name: str = "" - first_name: str = "" - last_name: str = "" - match: bool = False - is_business = request_owner.get("organizationName") - if is_business: - request_name = request_owner.get("organizationName").strip().upper() - elif ( - request_owner.get("individualName") - and request_owner["individualName"].get("first") - and request_owner["individualName"].get("last") - ): - first_name = request_owner["individualName"].get("first").strip().upper() - last_name = request_owner["individualName"].get("last").strip().upper() - if not request_name and not last_name: - return False - if registration.owner_groups: - for group in registration.owner_groups: - if group.status_type == MhrOwnerStatusTypes.ACTIVE: - for owner in group.owners: - if is_business and owner.business_name == request_name: - match = True - elif not is_business and owner.first_name == first_name and owner.last_name == last_name: - match = True - if not match and registration.change_registrations: # pylint: disable=too-many-nested-blocks - for reg in registration.change_registrations: - for group in reg.owner_groups: - if group.status_type == MhrOwnerStatusTypes.ACTIVE: - for owner in group.owners: - if is_business and owner.business_name == request_name: - match = True - elif not is_business and owner.first_name == first_name and owner.last_name == last_name: - match = True - return match - - -def get_existing_owner_groups(registration: MhrRegistration) -> dict: - """Get the existing active/exempt owner groups.""" - groups = [] - for existing in registration.owner_groups: - if existing.status_type in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): - groups.append(existing.json) - if registration.change_registrations: - for reg in registration.change_registrations: - if reg.owner_groups: - for existing in reg.owner_groups: - if existing.status_type in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): - groups.append(existing.json) - return groups - - -def is_valid_dealer_transfer_owner(registration: MhrRegistration, qs: dict) -> bool: - """Check qs dealer name matches owner name and owner is a sole owner.""" - qs_name: str = str(qs.get("businessName", "")).strip().upper() - dba_name: str = qs.get("dbaName", "") - if dba_name: - dba_name = dba_name.strip().upper() - logger.debug(f"is_valid_dealer_transfer_owner checking dealer name={qs_name} dba_name={dba_name}") - groups = get_existing_owner_groups(registration) - if not groups or len(groups) > 1: - return False - owners = groups[0].get("owners") - if owners and len(owners) == 1: - owner_name: str = owners[0].get("organizationName", "") - if owner_name: - logger.debug(f"Comparing owner name={owner_name} with qs name and dba name") - owner_name = owner_name.strip().upper() - if owner_name == qs_name or (dba_name and owner_name == dba_name): - return True - return False - - -def validate_delete_owners( # pylint: disable=too-many-branches - registration: MhrRegistration = None, json_data: dict = None -) -> str: - """Check groups id's and owners are valid for deleted groups.""" - error_msg = "" - if not registration or not json_data.get("deleteOwnerGroups"): - return error_msg - for deleted in json_data["deleteOwnerGroups"]: # pylint: disable=too-many-nested-blocks - if deleted.get("groupId"): - deleted_group = None - group_id = deleted["groupId"] - for existing in registration.owner_groups: - if existing.group_id == group_id: - deleted_group = existing - if not deleted_group and registration.change_registrations: - for reg in registration.change_registrations: - if reg.owner_groups: - for existing in reg.owner_groups: - if existing.group_id == group_id: - deleted_group = existing - if deleted_group: - tenancy_type = deleted.get("type") - # Data migration legacy owner group can have a status of EXEMPT. - if deleted_group.status_type not in (MhrOwnerStatusTypes.ACTIVE, MhrOwnerStatusTypes.EXEMPT): - error_msg += DELETE_GROUP_ID_INVALID.format(group_id=group_id) - if tenancy_type and deleted_group.tenancy_type != tenancy_type and tenancy_type != MhrTenancyTypes.NA: - error_msg += DELETE_GROUP_TYPE_INVALID.format(group_id=group_id) - else: - error_msg += DELETE_GROUP_ID_NONEXISTENT.format(group_id=group_id) - return error_msg - - -def delete_group(group_id: int, delete_groups): - """Check if owner group is flagged for deletion.""" - if not delete_groups or group_id < 1: - return False - for group in delete_groups: - if group.get("groupId", 0) == group_id: - return True - return False - - -def interest_required(groups, registration: MhrRegistration = None, delete_groups=None) -> bool: - """Determine if group interest is required.""" - group_count: int = len(groups) # Verify interest if multiple groups or existing interest. - if group_count > 1: - return True - if not registration: - return False - for existing in registration.owner_groups: - if ( - existing.status_type == MhrOwnerStatusTypes.ACTIVE - and existing.tenancy_type != MhrTenancyTypes.SOLE - and not delete_group(existing.group_id, delete_groups) - and existing.interest_denominator > 0 - ): - group_count += 1 - if registration.change_registrations: - for reg in registration.change_registrations: - if reg.owner_groups: - for existing in reg.owner_groups: - if ( - existing.status_type == MhrOwnerStatusTypes.ACTIVE - and existing.tenancy_type != MhrTenancyTypes.SOLE - and not delete_group(existing.group_id, delete_groups) - and existing.interest_denominator > 0 - ): - group_count += 1 - return group_count > 1 - - -def validate_group_interest( # pylint: disable=too-many-branches - groups, - denominator: int, - registration: MhrRegistration = None, - delete_groups=None, -): - """Verify owner group interest values are valid.""" - error_msg = "" - numerator_sum: int = 0 - group_count: int = len(groups) # Verify interest if multiple groups or existing interest. - if registration: # pylint: disable=too-many-nested-blocks - for existing in registration.owner_groups: - if ( - existing.status_type == MhrOwnerStatusTypes.ACTIVE - and existing.tenancy_type != MhrTenancyTypes.SOLE - and not delete_group(existing.group_id, delete_groups) - ): - den = existing.interest_denominator - if den > 0: - group_count += 1 - if den == denominator: - numerator_sum += existing.interest_numerator - elif den < denominator: - numerator_sum += denominator / den * existing.interest_numerator - else: - numerator_sum += int((denominator * existing.interest_numerator) / den) - if registration.change_registrations: - for reg in registration.change_registrations: - if reg.owner_groups: - for existing in reg.owner_groups: - if ( - existing.status_type == MhrOwnerStatusTypes.ACTIVE - and existing.tenancy_type != MhrTenancyTypes.SOLE - and not delete_group(existing.group_id, delete_groups) - ): - den = existing.interest_denominator - if den > 0: - group_count += 1 - if den == denominator: - numerator_sum += existing.interest_numerator - elif den < denominator: - numerator_sum += denominator / den * existing.interest_numerator - else: - numerator_sum += int((denominator * existing.interest_numerator) / den) - logger.debug(f"group_count={group_count} denominator={denominator}") - if group_count < 2: # Could have transfer of joint tenants with no interest. - return error_msg - for group in groups: - num = group.get("interestNumerator", 0) - den = group.get("interestDenominator", 0) - if num and den and num > 0 and den > 0: - if den == denominator: - numerator_sum += num - else: - numerator_sum += denominator / den * num - if numerator_sum != denominator: - error_msg = GROUP_INTEREST_MISMATCH - return error_msg - - -def get_modified_group(registration: MhrRegistration, group_id: int) -> dict: - """Find the existing owner group as JSON, matching on the group id.""" - group = {} - if not registration: - return group - for existing in registration.owner_groups: - if existing.group_id == group_id: - group = existing.json - break - if not group and registration.change_registrations: - for reg in registration.change_registrations: - if reg.owner_groups: - for existing in reg.owner_groups: - if existing.group_id == group_id: - group = existing.json - break - return group - - def validate_mhr_number(mhr_number: str, staff: bool) -> str: """Validate that a staff provide new MH mhr number is valid.""" error_msg = "" @@ -956,122 +693,6 @@ def has_active_permit(registration: MhrRegistration) -> bool: return False -def validate_owner_group(group, int_required: bool = False): - """Verify owner group is valid.""" - error_msg = "" - if not group: - return error_msg - tenancy_type: str = group.get("type", "") - if tenancy_type == MhrTenancyTypes.COMMON or int_required: - if not group.get("interestNumerator") or group.get("interestNumerator", 0) < 1: - error_msg += GROUP_NUMERATOR_MISSING - if not group.get("interestDenominator") or group.get("interestDenominator", 0) < 1: - error_msg += GROUP_DENOMINATOR_MISSING - if tenancy_type == MhrTenancyTypes.NA and group.get("owners") and len(group.get("owners")) > 1: - owner_count: int = 0 - for owner in group.get("owners"): - if not owner.get("partyType") or owner.get("partyType") in ( - MhrPartyTypes.OWNER_BUS, - MhrPartyTypes.OWNER_IND, - ): - owner_count += 1 - if owner_count != 0: - error_msg += TENANCY_TYPE_NA_INVALID2 - if tenancy_type == MhrTenancyTypes.JOINT and (not group.get("owners") or len(group.get("owners")) < 2): - error_msg += OWNERS_JOINT_INVALID - elif tenancy_type == MhrTenancyTypes.COMMON and (not group.get("owners") or len(group.get("owners")) > 1): - error_msg += OWNERS_COMMON_INVALID - elif tenancy_type == MhrTenancyTypes.SOLE and int_required: - error_msg += OWNERS_COMMON_SOLE_INVALID - return error_msg - - -def validate_owner(owner: dict) -> str: - """Verify owner names are valid and legacy suffix length is valid.""" - error_msg = "" - if not owner: - return error_msg - desc: str = "owner" - if owner.get("organizationName"): - error_msg += validate_text(owner.get("organizationName"), desc + " organization name") - elif owner.get("individualName"): - error_msg += validate_individual_name(owner.get("individualName"), desc) - return error_msg - - -def common_tenancy(groups, new: bool, active_count: int = 0) -> bool: - """Determine if the owner groups is a tenants in common scenario.""" - if new and groups and len(groups) == 1: - return False - for group in groups: - group_type = group.get("type", "") - if group_type and group_type != MhrTenancyTypes.SOLE and active_count > 1: - return True - return False - - -def validate_owner_groups_common(groups, registration: MhrRegistration = None, delete_groups=None): - """Verify tenants in common owner groups are valid.""" - error_msg = "" - tc_owner_count_invalid: bool = False - common_denominator: int = 0 - int_required: bool = interest_required(groups, registration, delete_groups) - for group in groups: - if common_denominator == 0: - common_denominator = group.get("interestDenominator", 0) - elif group.get("interestDenominator", 0) > common_denominator: - common_denominator = group.get("interestDenominator", 0) - if not group.get("owners"): - tc_owner_count_invalid = True - error_msg += validate_owner_group(group, int_required) - for owner in group.get("owners"): - error_msg += validate_owner(owner) - error_msg += validate_group_interest(groups, common_denominator, registration, delete_groups) - if tc_owner_count_invalid: - error_msg += OWNERS_COMMON_INVALID - return error_msg - - -def validate_owner_groups( - groups, new: bool, registration: MhrRegistration = None, delete_groups=None, active_count: int = 0 -): - """Verify owner groups are valid.""" - error_msg = "" - if not groups: - return error_msg - so_count: int = 0 - if common_tenancy(groups, new, active_count): - return validate_owner_groups_common(groups, registration, delete_groups) - for group in groups: - tenancy_type: str = group.get("type", "") - if new and tenancy_type == MhrTenancyTypes.COMMON: - error_msg += GROUP_COMMON_INVALID - error_msg += validate_owner_group(group, False) - for owner in group.get("owners"): - if tenancy_type == MhrTenancyTypes.SOLE: - so_count += 1 - error_msg += validate_owner(owner) - if so_count > 1 or (so_count == 1 and len(groups) > 1): - error_msg += ADD_SOLE_OWNER_INVALID - if not new and active_count == 1 and tenancy_type == MhrTenancyTypes.COMMON: - error_msg += GROUP_COMMON_INVALID - return error_msg - - -def get_active_group_count(json_data, registration: MhrRegistration) -> int: - """Count number of active owner groups.""" - group_count: int = 0 - if json_data.get("ownerGroups"): - group_count += len(json_data.get("ownerGroups")) - else: - if json_data.get("addOwnerGroups"): - group_count += len(json_data.get("addOwnerGroups")) - if json_data.get("deleteOwnerGroups"): - group_count -= len(json_data.get("deleteOwnerGroups")) - group_count += get_existing_group_count(registration) - return group_count - - def validate_cancel_permit(registration: MhrRegistration) -> str: """Validate an active, unexpired transport permit exists.""" error_msg: str = "" diff --git a/mhr-api/tests/unit/utils/test_admin_validator.py b/mhr-api/tests/unit/utils/test_admin_validator.py index 27a791d39..6b7272af8 100644 --- a/mhr-api/tests/unit/utils/test_admin_validator.py +++ b/mhr-api/tests/unit/utils/test_admin_validator.py @@ -19,7 +19,7 @@ from registry_schemas import utils as schema_utils from registry_schemas.example_data.mhr import DESCRIPTION -from mhr_api.utils import admin_validator as validator, validator_utils +from mhr_api.utils import admin_validator as validator, validator_utils, validator_owner_utils as val_owner_utils from mhr_api.models import MhrRegistration from mhr_api.models.type_tables import MhrDocumentTypes from mhr_api.services.authz import STAFF_ROLE @@ -479,14 +479,14 @@ (False, '000919', 'REGC_STAFF', None, None, None, DELETE_OG_VALID, validator.ADD_OWNERS_MISSING), (False, '000919', 'REGC_STAFF', None, None, ADD_OG_VALID, None, validator.DELETE_OWNERS_MISSING), (False, '000919', 'REGC_STAFF', None, None, ADD_OG_INVALID_JT, DELETE_OG_VALID, - validator_utils.OWNERS_JOINT_INVALID), + val_owner_utils.OWNERS_JOINT_INVALID), (True, '000912', 'EXRE', LOCATION_VALID, None, None, None, None), (True, '000912', 'EXRE', LOCATION_VALID_MINIMAL, None, None, None, None), (False, '000912', 'EXRE', LOCATION_000912, None, None, None, validator_utils.LOCATION_INVALID_IDENTICAL), (False, '000912', 'EXRE', None, DESCRIPTION_000912, None, None, validator_utils.DESCRIPTION_INVALID_IDENTICAL), (False, '000912', 'EXRE', None, None, None, DELETE_OG_000912, validator.ADD_OWNERS_MISSING), (False, '000912', 'EXRE', None, None, ADD_OG_VALID, None, validator.DELETE_OWNERS_MISSING), - (False, '000912', 'EXRE', None, None, ADD_OG_INVALID_JT, DELETE_OG_000912, validator_utils.OWNERS_JOINT_INVALID), + (False, '000912', 'EXRE', None, None, ADD_OG_INVALID_JT, DELETE_OG_000912, val_owner_utils.OWNERS_JOINT_INVALID), (True, '000912', 'EXRE', None, None, ADD_OG_VALID, DELETE_OG_000912, None), (True, '000912', 'EXRE', None, DESCRIPTION, None, None, None) ] diff --git a/mhr-api/tests/unit/utils/test_registration_validator.py b/mhr-api/tests/unit/utils/test_registration_validator.py index 92be6c372..4f5cd416c 100644 --- a/mhr-api/tests/unit/utils/test_registration_validator.py +++ b/mhr-api/tests/unit/utils/test_registration_validator.py @@ -20,6 +20,7 @@ from registry_schemas.example_data.mhr import REGISTRATION, TRANSFER, EXEMPTION from mhr_api.utils import registration_validator as validator, manufacturer_validator as man_validator, validator_utils +from mhr_api.utils import validator_owner_utils as val_owner_utils from mhr_api.models import MhrRegistration, MhrManufacturer, utils as model_utils from mhr_api.models.type_tables import MhrRegistrationTypes, MhrTenancyTypes, MhrDocumentTypes from mhr_api.models.utils import now_ts @@ -144,11 +145,7 @@ ('Reg invalid last name', None, 'first', 'middle', INVALID_TEXT_CHARSET, INVALID_CHARSET_MESSAGE, REGISTRATION), ('Reg street non utf-8', None, 'first', 'middle', 'last', None, REGISTRATION), ('Reg streetAdditional non utf-8', None, 'first', 'middle', 'last', None, REGISTRATION), - ('Reg city non utf-8', None, 'first', 'middle', 'last', None, REGISTRATION), - ('Trans invalid org/bus name', INVALID_TEXT_CHARSET, None, None, None, INVALID_CHARSET_MESSAGE, TRANSFER), - ('Trans invalid first name', None, INVALID_TEXT_CHARSET, 'middle', 'last', INVALID_CHARSET_MESSAGE, TRANSFER), - ('Trans invalid middle name', None, 'first', INVALID_TEXT_CHARSET, 'last', INVALID_CHARSET_MESSAGE, TRANSFER), - ('Trans invalid last name', None, 'first', 'middle', INVALID_TEXT_CHARSET, INVALID_CHARSET_MESSAGE, TRANSFER) + ('Reg city non utf-8', None, 'first', 'middle', 'last', None, REGISTRATION) ] # testdata pattern is ({description}, {park_name}, {dealer}, {pad}, {reserve_num}, {band_name}, {pid_num}, {message content}) TEST_LOCATION_DATA_MANUFACTURER = [ @@ -288,25 +285,25 @@ ('Valid TC', True, 1, 2, TC_GROUPS_VALID, None), ('Valid SO', True, None, None, SO_VALID, None), ('Valid JT', True, None, None, JT_VALID, None), - ('Invalid TC no owner', False, 1, 2, TC_GROUPS_VALID, validator_utils.OWNERS_COMMON_INVALID), - ('Invalid TC SO group', False, 1, 2, TC_GROUPS_VALID, validator_utils.OWNERS_COMMON_SOLE_INVALID), - ('Invalid TC 2 owners', False, 1, 2, TC_GROUPS_VALID, validator_utils.OWNERS_COMMON_INVALID), - ('Invalid TC only 1 group', False, 2, 2, TC_GROUPS_VALID, validator_utils.GROUP_COMMON_INVALID), - ('Invalid TC numerator missing', False, None, 2, TC_GROUPS_VALID, validator_utils.GROUP_NUMERATOR_MISSING), - ('Invalid TC numerator < 1', False, 0, 2, TC_GROUPS_VALID, validator_utils.GROUP_NUMERATOR_MISSING), - ('Invalid TC denominator missing', False, 1, None, TC_GROUPS_VALID, validator_utils.GROUP_DENOMINATOR_MISSING), - ('Invalid TC denominator < 1', False, 1, 0, TC_GROUPS_VALID, validator_utils.GROUP_DENOMINATOR_MISSING), - ('Invalid JT 1 owner', False, None, None, JT_OWNER_SINGLE, validator_utils.OWNERS_JOINT_INVALID), - ('Invalid SO 2 groups', False, None, None, SO_GROUP_MULTIPLE, validator_utils.ADD_SOLE_OWNER_INVALID), - ('Invalid SO 2 owners', False, None, None, SO_OWNER_MULTIPLE, validator_utils.ADD_SOLE_OWNER_INVALID) + ('Invalid TC no owner', False, 1, 2, TC_GROUPS_VALID, val_owner_utils.OWNERS_COMMON_INVALID), + ('Invalid TC SO group', False, 1, 2, TC_GROUPS_VALID, val_owner_utils.OWNERS_COMMON_SOLE_INVALID), + ('Invalid TC 2 owners', False, 1, 2, TC_GROUPS_VALID, val_owner_utils.OWNERS_COMMON_INVALID), + ('Invalid TC only 1 group', False, 2, 2, TC_GROUPS_VALID, val_owner_utils.GROUP_COMMON_INVALID), + ('Invalid TC numerator missing', False, None, 2, TC_GROUPS_VALID, val_owner_utils.GROUP_NUMERATOR_MISSING), + ('Invalid TC numerator < 1', False, 0, 2, TC_GROUPS_VALID, val_owner_utils.GROUP_NUMERATOR_MISSING), + ('Invalid TC denominator missing', False, 1, None, TC_GROUPS_VALID, val_owner_utils.GROUP_DENOMINATOR_MISSING), + ('Invalid TC denominator < 1', False, 1, 0, TC_GROUPS_VALID, val_owner_utils.GROUP_DENOMINATOR_MISSING), + ('Invalid JT 1 owner', False, None, None, JT_OWNER_SINGLE, val_owner_utils.OWNERS_JOINT_INVALID), + ('Invalid SO 2 groups', False, None, None, SO_GROUP_MULTIPLE, val_owner_utils.ADD_SOLE_OWNER_INVALID), + ('Invalid SO 2 owners', False, None, None, SO_OWNER_MULTIPLE, val_owner_utils.ADD_SOLE_OWNER_INVALID) ] # testdata pattern is ({description}, {valid}, {interest_data}, {common_denominator}, {message content}) TEST_DATA_GROUP_INTEREST = [ ('Valid 2 groups', True, INTEREST_VALID_1, 2, None), ('Valid 3 groups', True, INTEREST_VALID_2, 4, None), ('Valid 3 groups', True, INTEREST_VALID_3, 10, None), - ('Invalid numerator sum low', False, INTEREST_INVALID_1, 4, validator_utils.GROUP_INTEREST_MISMATCH), - ('Invalid numerator sum high', False, INTEREST_INVALID_2, 4, validator_utils.GROUP_INTEREST_MISMATCH) + ('Invalid numerator sum low', False, INTEREST_INVALID_1, 4, val_owner_utils.GROUP_INTEREST_MISMATCH), + ('Invalid numerator sum high', False, INTEREST_INVALID_2, 4, val_owner_utils.GROUP_INTEREST_MISMATCH) ] # testdata pattern is ({description}, {mhr_number}, {message content}) TEST_DATA_LIEN_COUNT = [ @@ -323,16 +320,16 @@ ('Valid TC exec', True, JT_VALID_EXEC, TC_VALID_EXEC, None, None, None), ('Valid TC trustee', True, JT_VALID_TRUSTEE, TC_VALID_TRUSTEE, None, None, None), ('Valid TC admin', True, JT_VALID_ADMIN, TC_VALID_ADMIN, None, None, None), - ('Invalid SO type NA', False, SO_VALID_EXEC, None, 'NA', None, validator.TENANCY_TYPE_NA_INVALID), - ('Invalid SO exec no desc', False, SO_VALID_EXEC, None, None, None, validator.OWNER_DESCRIPTION_REQUIRED), + ('Invalid SO type NA', False, SO_VALID_EXEC, None, 'NA', None, val_owner_utils.TENANCY_TYPE_NA_INVALID), + ('Invalid SO exec no desc', False, SO_VALID_EXEC, None, None, None, val_owner_utils.OWNER_DESCRIPTION_REQUIRED), ('Invalid common type JOINT', False, JT_VALID_EXEC, TC_VALID_EXEC, 'JOINT', None, - validator.TENANCY_PARTY_TYPE_INVALID), - ('Invalid JT party types 1', False, JT_VALID_EXEC, None, None, 'ADMINISTRATOR', validator.GROUP_PARTY_TYPE_INVALID), - ('Invalid JT party types 2', False, JT_VALID_TRUSTEE, None, None, 'EXECUTOR', validator.GROUP_PARTY_TYPE_INVALID), - ('Invalid JT party types 3', False, JT_VALID_ADMIN, None, None, 'TRUSTEE', validator.GROUP_PARTY_TYPE_INVALID), - ('Invalid JT type NA', False, JT_GROUP_VALID, None, 'NA', None, validator_utils.TENANCY_TYPE_NA_INVALID2), - ('Invalid NA type mix', False, JT_GROUP_MIX, None, None, None, validator_utils.TENANCY_TYPE_NA_INVALID2), - ('Invalid JT type mix', False, JT_GROUP_MIX, None, 'JT', None, validator.TENANCY_PARTY_TYPE_INVALID) + val_owner_utils.TENANCY_PARTY_TYPE_INVALID), + ('Invalid JT party types 1', False, JT_VALID_EXEC, None, None, 'ADMINISTRATOR', val_owner_utils.GROUP_PARTY_TYPE_INVALID), + ('Invalid JT party types 2', False, JT_VALID_TRUSTEE, None, None, 'EXECUTOR', val_owner_utils.GROUP_PARTY_TYPE_INVALID), + ('Invalid JT party types 3', False, JT_VALID_ADMIN, None, None, 'TRUSTEE', val_owner_utils.GROUP_PARTY_TYPE_INVALID), + ('Invalid JT type NA', False, JT_GROUP_VALID, None, 'NA', None, val_owner_utils.TENANCY_TYPE_NA_INVALID2), + ('Invalid NA type mix', False, JT_GROUP_MIX, None, None, None, val_owner_utils.TENANCY_TYPE_NA_INVALID2), + ('Invalid JT type mix', False, JT_GROUP_MIX, None, 'JT', None, val_owner_utils.TENANCY_PARTY_TYPE_INVALID) ] @@ -699,7 +696,7 @@ def test_validate_group_interest(session, desc, valid, interest_data, common_den group['interestNumerator'] = interest.get('numerator') group['interestDenominator'] = interest.get('denominator') json_data.append(group) - error_msg = validator_utils.validate_group_interest(json_data, common_den) + error_msg = val_owner_utils.validate_group_interest(json_data, common_den) if valid: assert error_msg == '' else: diff --git a/mhr-api/tests/unit/utils/test_transfer_validator.py b/mhr-api/tests/unit/utils/test_transfer_validator.py index 8130eb049..23a0c1b3e 100644 --- a/mhr-api/tests/unit/utils/test_transfer_validator.py +++ b/mhr-api/tests/unit/utils/test_transfer_validator.py @@ -19,7 +19,7 @@ from registry_schemas import utils as schema_utils from registry_schemas.example_data.mhr import TRANSFER -from mhr_api.utils import registration_validator as validator, validator_utils +from mhr_api.utils import registration_validator as validator, validator_utils, validator_owner_utils as val_owner_utils from mhr_api.models import MhrRegistration, utils as model_utils, MhrQualifiedSupplier from mhr_api.models.type_tables import MhrRegistrationStatusTypes, MhrTenancyTypes, MhrRegistrationTypes from mhr_api.models.type_tables import MhrPartyTypes @@ -86,9 +86,9 @@ #('Invalid draft', False, False, None, validator_utils.DRAFT_NOT_ALLOWED, MhrRegistrationStatusTypes.ACTIVE), # (DESC_INVALID_GROUP_ID, False, False, None, validator_utils.DELETE_GROUP_ID_INVALID, # MhrRegistrationStatusTypes.ACTIVE), - (DESC_NONEXISTENT_GROUP_ID, False, False, None, validator_utils.DELETE_GROUP_ID_NONEXISTENT, + (DESC_NONEXISTENT_GROUP_ID, False, False, None, val_owner_utils.DELETE_GROUP_ID_NONEXISTENT, MhrRegistrationStatusTypes.ACTIVE), - (DESC_INVALID_GROUP_TYPE, False, False, None, validator_utils.DELETE_GROUP_TYPE_INVALID, + (DESC_INVALID_GROUP_TYPE, False, False, None, val_owner_utils.DELETE_GROUP_TYPE_INVALID, MhrRegistrationStatusTypes.ACTIVE) ] # testdata pattern is ({description}, {valid}, {staff}, {tran_dt}, {dec_val}, {consideration}, {message content}) @@ -104,14 +104,14 @@ # testdata pattern is ({description}, {valid}, {numerator}, {denominator}, {add_group}, {message content}) TEST_TRANSFER_DATA_GROUP = [ ('Valid', True, 1, 2, None, None), - ('Invalid add TC no owner', False, None, None, TC_GROUP_TRANSFER_ADD2, validator_utils.OWNERS_COMMON_INVALID), - ('Invalid add JT 1 owner', False, None, None, JT_OWNER_SINGLE, validator_utils.OWNERS_JOINT_INVALID), - ('Invalid TC numerator missing', False, None, 2, TC_GROUPS_VALID, validator_utils.GROUP_NUMERATOR_MISSING), - ('Invalid TC numerator < 1', False, 0, 2, TC_GROUPS_VALID, validator_utils.GROUP_NUMERATOR_MISSING), - ('Invalid TC denominator missing', False, 1, None, TC_GROUPS_VALID, validator_utils.GROUP_DENOMINATOR_MISSING), - ('Invalid TC denominator < 1', False, 1, 0, TC_GROUPS_VALID, validator_utils.GROUP_DENOMINATOR_MISSING), - ('Invalid add SO 2 groups', False, None, None, SO_GROUP_MULTIPLE, validator_utils.ADD_SOLE_OWNER_INVALID), - ('Invalid add SO 2 owners', False, None, None, SO_OWNER_MULTIPLE, validator_utils.ADD_SOLE_OWNER_INVALID), + ('Invalid add TC no owner', False, None, None, TC_GROUP_TRANSFER_ADD2, val_owner_utils.OWNERS_COMMON_INVALID), + ('Invalid add JT 1 owner', False, None, None, JT_OWNER_SINGLE, val_owner_utils.OWNERS_JOINT_INVALID), + ('Invalid TC numerator missing', False, None, 2, TC_GROUPS_VALID, val_owner_utils.GROUP_NUMERATOR_MISSING), + ('Invalid TC numerator < 1', False, 0, 2, TC_GROUPS_VALID, val_owner_utils.GROUP_NUMERATOR_MISSING), + ('Invalid TC denominator missing', False, 1, None, TC_GROUPS_VALID, val_owner_utils.GROUP_DENOMINATOR_MISSING), + ('Invalid TC denominator < 1', False, 1, 0, TC_GROUPS_VALID, val_owner_utils.GROUP_DENOMINATOR_MISSING), + ('Invalid add SO 2 groups', False, None, None, SO_GROUP_MULTIPLE, val_owner_utils.ADD_SOLE_OWNER_INVALID), + ('Invalid add SO 2 owners', False, None, None, SO_OWNER_MULTIPLE, val_owner_utils.ADD_SOLE_OWNER_INVALID), # ('Invalid add TC > 1 owner', False, None, None, TC_GROUP_TRANSFER_ADD, validator.OWNERS_COMMON_INVALID) ] # testdata pattern is ({description},{valid},{mhr_num},{account_id},{delete_groups},{add_groups},{message content}) @@ -137,9 +137,9 @@ ('Invalid tenancy type', False, '000920', 'PS12345', SO_GROUP, TRAND_ADD_GROUPS, validator.TRAN_DEATH_JOINT_TYPE), ('Invalid add 2 groups', False, '000920', 'PS12345', TRAND_DELETE_GROUPS, TRAND_ADD_GROUPS, - validator.TRAN_DEATH_GROUP_COUNT), + val_owner_utils.TRAN_DEATH_GROUP_COUNT), ('Invalid delete 2 groups', False, '000920', 'PS12345', TRAND_DELETE_GROUPS, TRAND_ADD_GROUPS, - validator.TRAN_DEATH_GROUP_COUNT), + val_owner_utils.TRAN_DEATH_GROUP_COUNT), ('Invalid future death ts', False, '000920', 'PS12345', TRAND_DELETE_GROUPS, TRAND_ADD_GROUPS, validator.TRAN_DEATH_DATE_INVALID), ('Valid JOINT BUS non-QS', False, '000920', 'PS12345', TRAND_DELETE_GROUPS, TRAND_ADD_GROUPS_JOINT, None), @@ -167,9 +167,9 @@ ('Invalid no death info', False, '000921', 'PS12345', ADMIN_DELETE_GROUPS, ADMIN_ADD_GROUPS, validator.TRAN_ADMIN_DEATH_CERT, True), ('Invalid add 2 groups', False, '000921', 'PS12345', ADMIN_DELETE_GROUPS, ADMIN_ADD_GROUPS, - validator.TRAN_DEATH_GROUP_COUNT, True), + val_owner_utils.TRAN_DEATH_GROUP_COUNT, True), ('Invalid delete 2 groups', False, '000921', 'PS12345', ADMIN_DELETE_GROUPS, ADMIN_ADD_GROUPS, - validator.TRAN_DEATH_GROUP_COUNT, True) + val_owner_utils.TRAN_DEATH_GROUP_COUNT, True) ] # testdata pattern is ({description},{valid},{mhr_num},{account_id},{delete_groups},{add_groups},{message content},{staff}) TEST_TRANSFER_DATA_AFFIDAVIT = [ @@ -194,9 +194,9 @@ ('Invalid no death date', False, '000921', 'PS12345', EXEC_DELETE_GROUPS, EXEC_ADD_GROUPS, validator.TRAN_DEATH_DATE_MISSING, True), ('Invalid add 2 groups', False, '000921', 'PS12345', EXEC_DELETE_GROUPS, EXEC_ADD_GROUPS, - validator.TRAN_DEATH_GROUP_COUNT, True), + val_owner_utils.TRAN_DEATH_GROUP_COUNT, True), ('Invalid delete 2 groups', False, '000921', 'PS12345', EXEC_DELETE_GROUPS, EXEC_ADD_GROUPS, - validator.TRAN_DEATH_GROUP_COUNT, True) + val_owner_utils.TRAN_DEATH_GROUP_COUNT, True) ] # testdata pattern is ({description},{valid},{mhr_num},{account_id},{delete_groups},{add_groups},{message content},{staff}) TEST_TRANSFER_DATA_WILL = [ @@ -204,8 +204,7 @@ ('Valid delete EXEC', True, '000923', 'PS12345', WILL_DELETE_GROUPS3, EXEC_ADD_GROUPS, None, True), ('Invalid non-staff', False, '000921', 'PS12345', WILL_DELETE_GROUPS, EXEC_ADD_GROUPS, validator.REG_STAFF_ONLY, False), - ('Invalid add owner', False, '000921', 'PS12345', WILL_DELETE_GROUPS, EXEC_ADD_GROUPS, - validator.TRAN_WILL_NEW_OWNER, True), + ('Valid add owner', True, '000921', 'PS12345', WILL_DELETE_GROUPS, EXEC_ADD_GROUPS, None, True), ('Valid party type EXECUTOR', True, '000921', 'PS12345', WILL_DELETE_GROUPS, EXEC_ADD_GROUPS, None, True), ('Valid party type TRUSTEE', True, '000921', 'PS12345', WILL_DELETE_GROUPS, EXEC_ADD_GROUPS, None, True), ('Valid party type ADMINISTRATOR', True, '000921', 'PS12345', WILL_DELETE_GROUPS, EXEC_ADD_GROUPS, None, True), @@ -218,22 +217,22 @@ ('Invalid no death info', False, '000921', 'PS12345', WILL_DELETE_GROUPS2, EXEC_ADD_GROUPS, validator.TRAN_WILL_DEATH_CERT, True), ('Invalid add 2 groups', False, '000921', 'PS12345', WILL_DELETE_GROUPS, EXEC_ADD_GROUPS, - validator.TRAN_DEATH_GROUP_COUNT, True), + val_owner_utils.TRAN_DEATH_GROUP_COUNT, True), ('Invalid delete 2 groups', False, '000921', 'PS12345', WILL_DELETE_GROUPS, EXEC_ADD_GROUPS, - validator.TRAN_DEATH_GROUP_COUNT, True) + val_owner_utils.TRAN_DEATH_GROUP_COUNT, True) ] # testdata pattern is ({description}, {valid}, {mhr_num}, {tenancy_type}, {add_group}, {message content}) TEST_TRANSFER_DEATH_NA_DATA = [ ('Invalid JOINT tenancy-party type', False, '000919', MhrTenancyTypes.JOINT, TC_GROUP_TRANSFER_ADD, - validator.TENANCY_PARTY_TYPE_INVALID), + val_owner_utils.TENANCY_PARTY_TYPE_INVALID), ('Invalid tenancy type - party type', False, '000900', MhrTenancyTypes.JOINT, TC_GROUP_TRANSFER_ADD, - validator.TENANCY_PARTY_TYPE_INVALID) + val_owner_utils.TENANCY_PARTY_TYPE_INVALID) ] # testdata pattern is ({description}, {valid}, {numerator}, {denominator}, {message content}) TEST_TRANSFER_DATA_GROUP_INTEREST = [ ('Valid add', True, 1, 2, None), - ('Invalid numerator < 1', False, 1, 4, validator_utils.GROUP_INTEREST_MISMATCH), - ('Invalid numerator sum high', False, 3, 4, validator_utils.GROUP_INTEREST_MISMATCH) + ('Invalid numerator < 1', False, 1, 4, val_owner_utils.GROUP_INTEREST_MISMATCH), + ('Invalid numerator sum high', False, 3, 4, val_owner_utils.GROUP_INTEREST_MISMATCH) ] # testdata pattern is ({description}, {valid}, {staff}, {kc_group}, {mhr_num}, {json_data}, {message content}) TEST_TRANSFER_DATA_QS = [ @@ -248,9 +247,10 @@ ('Valid existing exec', True, True, 'COMMON', '000924', TRANS_TC_4, None), ('Valid unchanged exec', True, True, 'COMMON', '000924', TRANS_TC_5, None), ('Valid split exec', True, True, 'COMMON', '000924', TRANS_TC_6, None), - ('Invalid add TC type', False, True, 'COMMON', '000900', TRANS_TC_3, validator_utils.GROUP_COMMON_INVALID), - ('Invalid add NA type', False, True, 'NA', '000900', TRANS_TC_3, validator.TENANCY_TYPE_NA_INVALID), - ('Invalid add exec', False, True, 'COMMON', '000924', TRANS_TC_5, validator.TRANSFER_PARTY_TYPE_INVALID), + ('Invalid add TC type', False, True, 'COMMON', '000900', TRANS_TC_3, val_owner_utils.GROUP_COMMON_INVALID), + ('Invalid add NA type', False, True, 'NA', '000900', TRANS_TC_3, val_owner_utils.TENANCY_TYPE_NA_INVALID), + ('Invalid add exec', False, False, 'COMMON', '000924', TRANS_TC_5, val_owner_utils.TRANSFER_PARTY_TYPE_INVALID), + ('Valid staff add exec', True, True, 'COMMON', '000924', TRANS_TC_5, None), ('Valid add exec staff misc.', True, True, 'COMMON', '000924', TRANS_TC_5, None) ] # testdata pattern is ({desc}, {valid}, {doc_type}, {reg_type}, {message content}) @@ -274,9 +274,10 @@ ('Valid TAXS', True, 'TAXS', MhrRegistrationTypes.TRANS, None), ('Valid VEST', True, 'VEST', MhrRegistrationTypes.TRANS, None), ('Valid EXECUTOR', True, 'TRANS_LAND_TITLE', MhrRegistrationTypes.TRANS, None), + ('Valid EXECUTOR no doc type', True, None, MhrRegistrationTypes.TRANS, None), ('Valid ADMINISTRATOR', True, 'ABAN', MhrRegistrationTypes.TRANS, None), ('Valid TRUSTEE', True, 'TRANS_QUIT_CLAIM', MhrRegistrationTypes.TRANS, None), - ('Invalid no type EXECUTOR', False, None, None, validator.TRANSFER_PARTY_TYPE_INVALID), + ('Invalid no type EXECUTOR', False, None, None, val_owner_utils.TRANSFER_PARTY_TYPE_INVALID), ('Invalid doc type', False, 'WILL', MhrRegistrationTypes.TRANS, 'data validation errors'), ('Invalid reg type', False, 'TRANS_TRUST', MhrRegistrationTypes.TRAND, validator.TRANS_DOC_TYPE_INVALID), ('Invalid not staff', False, 'ABAN', MhrRegistrationTypes.TRANS, validator.TRANS_DOC_TYPE_NOT_ALLOWED) @@ -616,8 +617,9 @@ def test_validate_transfer_will(session, desc, valid, mhr_num, account_id, delet json_data['deleteOwnerGroups'][0]['owners'][0]['partyType'] = MhrPartyTypes.ADMINISTRATOR elif desc == 'Invalid party type add': json_data['addOwnerGroups'][0]['owners'][0]['partyType'] = MhrPartyTypes.TRUSTEE - elif desc == 'Invalid add owner': + elif desc == 'Valid add owner': json_data['addOwnerGroups'][0]['owners'].append(ADD_OWNER) + json_data['addOwnerGroups'][0]['type'] = 'NA' elif desc == 'Invalid executor missing': del json_data['addOwnerGroups'][0]['owners'][1]['partyType'] del json_data['addOwnerGroups'][0]['owners'][1]['description'] @@ -628,7 +630,7 @@ def test_validate_transfer_will(session, desc, valid, mhr_num, account_id, delet valid_format, errors = schema_utils.validate(json_data, 'transfer', 'mhr') # Additional validation not covered by the schema. - registration: MhrRegistration = MhrRegistration.find_by_mhr_number(mhr_num, account_id) + registration: MhrRegistration = MhrRegistration.find_by_mhr_number(mhr_num, account_id, staff) error_msg = validator.validate_transfer(registration, json_data, staff, STAFF_ROLE) current_app.logger.info(error_msg) if errors: @@ -725,9 +727,11 @@ def test_validate_transfer_tc(session, desc, valid, staff, gtype, mhr_num, data, json_data = copy.deepcopy(data) json_data['documentId'] = DOC_ID_VALID json_data['addOwnerGroups'][0]['type'] = gtype - if desc == 'Invalid add exec': + if desc in ('Invalid add exec', 'Valid staff add exec'): json_data['addOwnerGroups'][1]['owners'][0]['partyType'] = 'EXECUTOR' json_data['addOwnerGroups'][1]['owners'][0]['description'] = 'EXECUTOR OF THE ESTATED OF ...' + if desc == 'Valid staff add exec': + json_data['addOwnerGroups'][1]['type'] = 'NA' elif desc == 'Valid add exec staff misc.': json_data['transferDocumentType'] = 'TRANS_INFORMAL_SALE' json_data['addOwnerGroups'][1]['owners'][0]['partyType'] = 'EXECUTOR' @@ -766,9 +770,12 @@ def test_validate_transfer_doc_type(session, desc, valid, doc_type, reg_type, me if desc == 'Invalid not staff': staff = False group = QUALIFIED_USER_GROUP - elif desc in ('Invalid no type EXECUTOR', 'Valid EXECUTOR'): + elif desc in ('Invalid no type EXECUTOR', 'Valid EXECUTOR', 'Valid EXECUTOR no doc type'): json_data['addOwnerGroups'][0]['owners'][0]['partyType'] = 'EXECUTOR' json_data['addOwnerGroups'][0]['owners'][0]['description'] = 'EXECUTOR OF THE ESTATED OF ...' + if desc == 'Invalid no type EXECUTOR': + staff = False + group = QUALIFIED_USER_GROUP elif desc == 'Valid ADMINISTRATOR': json_data['addOwnerGroups'][0]['owners'][0]['partyType'] = 'ADMINISTRATOR' json_data['addOwnerGroups'][0]['owners'][0]['description'] = 'ADMINISTRATOR OF THE ESTATED OF ...' diff --git a/ppr-api/src/ppr_api/resources/financing_utils.py b/ppr-api/src/ppr_api/resources/financing_utils.py index c5c30c2dc..d6b6d7f9e 100644 --- a/ppr-api/src/ppr_api/resources/financing_utils.py +++ b/ppr-api/src/ppr_api/resources/financing_utils.py @@ -307,7 +307,7 @@ def get_registration_callback_report(registration: Registration): # pylint: dis report_info.save() # Track success event. EventTracking.create(registration_id, EventTracking.EventTrackingTypes.REGISTRATION_REPORT, int(HTTPStatus.OK)) - return response, HTTPStatus.OK + return {}, HTTPStatus.OK except ReportException as report_err: return registration_callback_error( resource_utils.CallbackExceptionCodes.REPORT_ERR, @@ -372,7 +372,7 @@ def get_registration_report( # pylint: disable=too-many-return-statements,too-m doc_name = model_utils.get_doc_storage_name(registration) logger.info(f"Saving registration report output to doc storage: name={doc_name}.") response = GoogleStorageService.save_document(doc_name, raw_data, DocumentTypes.REGISTRATION) - logger.info("Save document storage response: " + json.dumps(response)) + logger.info(f"Save document storage response: {response}") report_info.create_ts = model_utils.now_ts() report_info.doc_storage_url = doc_name report_info.save() @@ -393,7 +393,7 @@ def get_registration_report( # pylint: disable=too-many-return-statements,too-m doc_name = model_utils.get_doc_storage_name(registration) logger.info(f"Saving registration report output to doc storage: name={doc_name}.") response = GoogleStorageService.save_document(doc_name, raw_data, DocumentTypes.REGISTRATION) - logger.info("Save document storage response: " + json.dumps(response)) + logger.info(f"Save document storage response: {response}") verification_report: VerificationReport = VerificationReport( create_ts=model_utils.now_ts(), registration_id=registration.id, diff --git a/ppr-api/src/ppr_api/resources/v1/callbacks.py b/ppr-api/src/ppr_api/resources/v1/callbacks.py index 82744c8b5..e93256d86 100644 --- a/ppr-api/src/ppr_api/resources/v1/callbacks.py +++ b/ppr-api/src/ppr_api/resources/v1/callbacks.py @@ -152,7 +152,7 @@ def generate_mail_callback_report(mail_report: MailReport): # pylint: disable=t ) logger.info(f"Saving mail report output to doc storage: name={doc_name}.") response = GoogleStorageService.save_document(doc_name, raw_data, DocumentTypes.MAIL_DEFAULT) - logger.info("Save document storage response: " + json.dumps(response)) + logger.info(f"Save document storage response: {response}.") mail_report.update_storage_url(doc_name, HTTPStatus.OK) mail_report.save() return {}, HTTPStatus.OK, {"Content-Type": "application/json"} diff --git a/ppr-api/src/ppr_api/resources/v1/search_results.py b/ppr-api/src/ppr_api/resources/v1/search_results.py index 77c90c029..b8c768f26 100644 --- a/ppr-api/src/ppr_api/resources/v1/search_results.py +++ b/ppr-api/src/ppr_api/resources/v1/search_results.py @@ -204,7 +204,7 @@ def stream(begin, end, raw_data): doc_name = model_utils.get_search_doc_storage_name(search_detail.search) logger.info(f"Saving report output to doc storage: name={doc_name}.") response = GoogleStorageService.save_document(doc_name, raw_data) - logger.info("Save document storage response: " + json.dumps(response)) + logger.info(f"Save document storage response: {response}.") search_detail.doc_storage_url = doc_name search_detail.save() return raw_data, HTTPStatus.OK, {"Content-Type": "application/pdf"} @@ -402,7 +402,7 @@ def results_pdf_response( if raw_data and status_code in (HTTPStatus.OK, HTTPStatus.CREATED): doc_name = model_utils.get_search_doc_storage_name(search_detail.search) response = GoogleStorageService.save_document(doc_name, raw_data) - logger.info(f"Save {doc_name} document storage response: " + json.dumps(response)) + logger.info(f"Save {doc_name} document storage response: {response}") search_detail.doc_storage_url = doc_name search_detail.save() return raw_data, HTTPStatus.OK, {"Content-Type": "application/pdf"}