Skip to content

Commit

Permalink
Enhance MH registration location data validation (#1292)
Browse files Browse the repository at this point in the history
Signed-off-by: Doug Lovett <doug@diamante.ca>
  • Loading branch information
doug-lovett authored Apr 24, 2023
1 parent b8208c6 commit f39a3b9
Show file tree
Hide file tree
Showing 7 changed files with 397 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{% elif addOwnerGroups is defined and addOwnerGroups[0].type == 'SOLE' %}
Sole Owner
{% elif addOwnerGroups is defined and addOwnerGroups[0].type == 'JOINT' and
(addOwnerGroups[0].interestDenominator is not defined or addOwnerGroups[0].interestDenominator == '') %}
(addOwnerGroups[0].interestDenominator is not defined or addOwnerGroups[0].interestDenominator == 0) %}
Joint Tenants
{% elif addOwnerGroups is defined %}
Tenants in Common
Expand All @@ -23,7 +23,7 @@
</span>
</div>
{% if addOwnerGroups is defined and (addOwnerGroups[0].type == 'COMMON' or
(addOwnerGroups[0].interestDenominator is defined and addOwnerGroups[0].interestDenominator != '')) %}
(addOwnerGroups[0].interestDenominator is defined and addOwnerGroups[0].interestDenominator != 0)) %}
<div class="separator-section mt-3"></div>
{% elif ownerGroups is defined and ownerGroups|length > 1 %}
<div class="separator-section mt-3"></div>
Expand Down
1 change: 1 addition & 0 deletions mhr_api/src/mhr_api/models/mhr_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class MhrDocument(db.Model): # pylint: disable=too-many-instance-attributes
consideration_value = db.Column('consideration_value', db.String(80), nullable=True)
consent = db.Column('consent', db.String(60), nullable=True)
transfer_date = db.Column('transfer_date', db.DateTime, nullable=True)
affirm_by = db.Column('affirm_by', db.String(60), nullable=True)

# parent keys
registration_id = db.Column('registration_id', db.Integer, db.ForeignKey('mhr_registrations.id'), nullable=False,
Expand Down
4 changes: 2 additions & 2 deletions mhr_api/src/mhr_api/models/mhr_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,8 +217,8 @@ def registration_json(self) -> dict:
note['remarks'] != 'MANUFACTURED HOME REGISTRATION CANCELLED':
# Only staff can see remarks if not default.
note['remarks'] = 'MANUFACTURED HOME REGISTRATION CANCELLED'
elif doc_type == 'TAXN' and note.get('status') != 'A': # Conditionally display remarks.
note['remarks'] = ''
elif doc_type == 'TAXN' and note.get('status') != 'A': # Conditionally include if active.
include = False
# Exclude if expiry elapsed.
elif doc_type in ('CAU', 'CAUC', 'CAUE') and note.get('expiryDate') and \
model_utils.date_elapsed(note.get('expiryDate')):
Expand Down
89 changes: 76 additions & 13 deletions mhr_api/src/mhr_api/utils/registration_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,13 @@
RESERVE_NUMBER_REQUIRED = 'The location Indian Reserve number is required for this registration. '
OWNERS_JOINT_INVALID = 'The owner group must contain at least 2 owners. '
OWNERS_COMMON_INVALID = 'Each COMMON owner group must contain exactly 1 owner. '
LOCATION_DEALER_REQUIRED = 'The location dealer name is required for this registration. '
LOCATION_DEALER_REQUIRED = 'Location dealer/manufacturer name is required for this registration. '
STATUS_CONFIRMATION_REQUIRED = 'The land status confirmation is required for this registration. '
LOCATION_PARK_NAME_REQUIRED = 'The location park name is required for this registration. '
LOCATION_PARK_NAME_REQUIRED = 'Location park name is required for this registration. '
LOCATION_PARK_PAD_REQUIRED = 'Location park PAD is required for this registration. '
LOCATION_STRATA_REQUIRED = 'Location parcel ID or all of lot, plan, land district are required for this registration. '
LOCATION_OTHER_REQUIRED = 'Location parcel ID or all of lot, plan, land district or all of land district, district ' \
'lot are required for this registration. '
LOCATION_ADDRESS_MISMATCH = 'The existing location address must match the current location address. '
OWNER_NAME_MISMATCH = 'The existing owner name must match exactly a current owner name for this registration. '
MANUFACTURER_DEALER_INVALID = 'The existing location must be a dealer or manufacturer lot for this registration. '
Expand Down Expand Up @@ -95,6 +99,15 @@
TRAN_EXEC_DEATH_CERT = 'All deceased owners must have a death certificate. '
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. '
LOCATION_MANUFACTURER_ALLOWED = 'Park name, PAD, band name, reserve number, parcel ID, and LTSA details are ' \
'not allowed with a MANUFACTURER location type. '
LOCATION_PARK_ALLOWED = 'Dealer/manufacturer name, band name, reserve number, parcel ID, and LTSA details are ' \
'not allowed with a MH_PARK location type. '
LOCATION_RESERVE_ALLOWED = 'Dealer/manufacturer name, park name, and PAD are not allowed with a RESERVE location type. '
LOCATION_STRATA_ALLOWED = 'Dealer/manufacturer name, park name, PAD, band name, and reserve number are not allowed ' \
'with a STRATA location type. '
LOCATION_OTHER_ALLOWED = 'Dealer/manufacturer name, park name, PAD, band name, and reserve number are not allowed ' \
'with an OTHER location type. '


def validate_registration(json_data, staff: bool = False):
Expand All @@ -108,7 +121,7 @@ def validate_registration(json_data, staff: bool = False):
error_msg += validate_submitting_party(json_data)
owner_count: int = len(json_data.get('ownerGroups')) if json_data.get('ownerGroups') else 0
error_msg += validate_owner_groups(json_data.get('ownerGroups'), True, None, None, owner_count)
error_msg += validate_location(json_data)
error_msg += validate_location(json_data.get('location'))
except Exception as validation_exception: # noqa: B902; eat all errors
current_app.logger.error('validate_registration exception: ' + str(validation_exception))
error_msg += VALIDATOR_ERROR
Expand Down Expand Up @@ -187,9 +200,9 @@ def validate_permit(registration: MhrRegistration, json_data, staff: bool = Fals
error_msg += validate_submitting_party(json_data)
error_msg += validate_registration_state(registration, staff, MhrRegistrationTypes.PERMIT)
error_msg += validate_draft_state(json_data)
error_msg += validate_location(json_data)
if json_data.get('newLocation'):
location = json_data.get('newLocation')
error_msg += validate_location(location)
error_msg += validate_tax_certificate(location, current_location)
if not json_data.get('landStatusConfirmation'):
if location.get('locationType') and \
Expand Down Expand Up @@ -632,28 +645,78 @@ def validate_owner_groups_common(groups, registration: MhrRegistration = None, d
return error_msg


def validate_location(json_data):
"""Verify location values are valid."""
def validate_location(location): # pylint: disable=too-many-branches
"""Verify the combination of location values is valid."""
error_msg = ''
if not json_data.get('location') and not json_data.get('newLocation'):
# No point validating if no no required locationType.
if not location or not location.get('locationType'):
return error_msg
location = json_data.get('location')
if not location:
location = json_data.get('newLocation')
if location.get('locationType') and location['locationType'] == MhrLocationTypes.RESERVE:
loc_type = location['locationType']
if loc_type == MhrLocationTypes.RESERVE:
if not location.get('bandName'):
error_msg += BAND_NAME_REQUIRED
if not location.get('reserveNumber'):
error_msg += RESERVE_NUMBER_REQUIRED
elif location.get('locationType') and location['locationType'] == MhrLocationTypes.MANUFACTURER:
elif loc_type == MhrLocationTypes.MANUFACTURER:
if not location.get('dealerName'):
error_msg += LOCATION_DEALER_REQUIRED
elif location.get('locationType') and location['locationType'] == MhrLocationTypes.MH_PARK:
elif loc_type == MhrLocationTypes.MH_PARK:
if not location.get('parkName'):
error_msg += LOCATION_PARK_NAME_REQUIRED
if not location.get('pad'):
error_msg += LOCATION_PARK_PAD_REQUIRED
elif loc_type == MhrLocationTypes.STRATA:
if not location.get('pidNumber') and \
(not location.get('lot') or not location.get('plan') or not location.get('landDistrict')):
error_msg += LOCATION_STRATA_REQUIRED
elif loc_type == MhrLocationTypes.OTHER and not location.get('pidNumber'):
if not location.get('landDistrict'):
error_msg += LOCATION_OTHER_REQUIRED
elif location.get('plan') and location.get('lot'):
error_msg += ''
elif not location.get('districtLot'):
error_msg += LOCATION_OTHER_REQUIRED
error_msg += validate_location_allowed(location, loc_type)
return error_msg


def validate_location_allowed(location, loc_type):
"""Verify the allowed location values by location type."""
error_msg = ''
if loc_type == MhrLocationTypes.MANUFACTURER:
if location.get('bandName') or location.get('parkName') or location.get('reserveNumber') or \
location.get('pad') or has_location_ltsa_details(location):
error_msg = LOCATION_MANUFACTURER_ALLOWED
elif loc_type == MhrLocationTypes.MH_PARK and \
(location.get('bandName') or location.get('reserveNumber') or
location.get('dealerName') or has_location_ltsa_details(location)):
error_msg = LOCATION_PARK_ALLOWED
elif loc_type == MhrLocationTypes.RESERVE and \
(location.get('dealerName') or location.get('parkName') or location.get('pad')):
error_msg = LOCATION_RESERVE_ALLOWED
elif loc_type in (MhrLocationTypes.STRATA, MhrLocationTypes.OTHER):
if location.get('dealerName') or location.get('parkName') or location.get('pad') or \
location.get('bandName') or location.get('reserveNumber'):
if loc_type == MhrLocationTypes.STRATA:
error_msg = LOCATION_STRATA_ALLOWED
else:
error_msg = LOCATION_OTHER_ALLOWED
return error_msg


def has_location_ltsa_details(location) -> bool:
"""Verify the location has ltsa detail properties."""
if location.get('lot') or location.get('parcel') or location.get('block') or location.get('districtLot') or\
location.get('partOf'):
return True
if location.get('section') or location.get('township') or location.get('range') or location.get('plan') or \
location.get('meridian'):
return True
if location.get('pidNumber') or location.get('legalDescription') or location.get('landDistrict'):
return True
return False


def validate_individual_name(name_json, desc: str = ''):
"""Verify individual name is valid."""
error_msg = validate_text(name_json.get('first'), desc + ' first')
Expand Down
16 changes: 15 additions & 1 deletion mhr_api/tests/unit/api/test_registrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,20 @@

MOCK_AUTH_URL = 'https://bcregistry-bcregistry-mock.apigee.net/mockTarget/auth/api/v1/'
MOCK_PAY_URL = 'https://bcregistry-bcregistry-mock.apigee.net/mockTarget/pay/api/v1/'

LOCATION = {
'locationType': 'MH_PARK',
'address': {
'street': '1117 GLENDALE AVENUE',
'city': 'SALMO',
'region': 'BC',
'country': 'CA',
'postalCode': ''
},
'leaveProvince': False,
'parkName': 'GLENDALE TRAILER PARK',
'pad': '2',
'additionalDescription': 'TEST PARK'
}
# testdata pattern is ({desc}, {roles}, {status}, {has_account}, {results_size})
TEST_GET_ACCOUNT_DATA = [
('Missing account', [MHR_ROLE], HTTPStatus.BAD_REQUEST, False, 0),
Expand Down Expand Up @@ -136,6 +149,7 @@ def test_create(session, client, jwt, desc, has_submitting, roles, status, has_a
current_app.config.update(AUTH_SVC_URL=MOCK_AUTH_URL)
headers = None
json_data = copy.deepcopy(REGISTRATION)
json_data['location'] = copy.deepcopy(LOCATION)
if not has_submitting:
del json_data['submittingParty']
if has_account:
Expand Down
4 changes: 2 additions & 2 deletions mhr_api/tests/unit/utils/test_permit_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,8 +205,7 @@
'postalCode': ''
},
'leaveProvince': False,
'dealerName': '',
'pad': '2'
'dealerName': ''
}
LOCATION_PID = {
'locationType': 'STRATA',
Expand Down Expand Up @@ -430,6 +429,7 @@ def test_validate_pid(session, desc, pid, valid, message_content):
json_data = get_valid_registration()
if json_data.get('documentId'):
del json_data['documentId']
json_data['newLocation'] = copy.deepcopy(LOCATION_PID)
json_data['newLocation']['pidNumber'] = pid
# current_app.logger.info(json_data)
valid_format, errors = schema_utils.validate(json_data, 'permit', 'mhr')
Expand Down
Loading

0 comments on commit f39a3b9

Please sign in to comment.