Skip to content

Commit

Permalink
Allow QS dealer transfers. (#1990)
Browse files Browse the repository at this point in the history
* Allow QS dealer transfers.

Signed-off-by: Doug Lovett <[email protected]>

* Allow QS dealer transfers.

Signed-off-by: Doug Lovett <[email protected]>

* Exemption report add owner suffix/description.

Signed-off-by: Doug Lovett <[email protected]>

---------

Signed-off-by: Doug Lovett <[email protected]>
  • Loading branch information
doug-lovett authored Jul 22, 2024
1 parent c8592b0 commit 3ec06ad
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 37 deletions.
2 changes: 2 additions & 0 deletions mhr_api/report-templates/exemptionV2.html
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@
{{ party.individualName.first }}
{% if party.individualName.middle is defined %}&nbsp;{{ party.individualName.middle }}{% endif %}
{% endif %}
{% if party.suffix is defined and party.suffix != '' %}&nbsp;{{ party.suffix }}{% endif %}
{% if party.description is defined and party.description != '' %}&nbsp;{{ party.description }}{% endif %}
{% if not loop.last %}
<div></div>
{% endif %}
Expand Down
4 changes: 3 additions & 1 deletion mhr_api/src/mhr_api/models/db2/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,9 @@ def find_by_mhr_number(cls, mhr_number: str):
documents = None
if mhr_number:
try:
documents = db.session.query(Db2Document).filter(Db2Document.mhr_number == mhr_number).all()
documents = db.session.query(Db2Document) \
.filter(Db2Document.mhr_number == mhr_number) \
.order_by(Db2Document.registration_ts).all()
if documents:
for doc in documents:
doc.strip()
Expand Down
28 changes: 18 additions & 10 deletions mhr_api/src/mhr_api/resources/v1/transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
"""API endpoints for requests to maintain MH transfer of sale/ownership requests."""

from http import HTTPStatus

from flask import Blueprint
Expand All @@ -29,8 +28,8 @@
get_group,
is_reg_staff_account
)
from mhr_api.services.authz import TRANSFER_SALE_BENEFICIARY, TRANSFER_DEATH_JT
from mhr_api.models import MhrRegistration
from mhr_api.services.authz import TRANSFER_SALE_BENEFICIARY, TRANSFER_DEATH_JT, DEALERSHIP_GROUP
from mhr_api.models import MhrQualifiedSupplier, MhrRegistration
from mhr_api.models import registration_utils as model_reg_utils, utils as model_utils
from mhr_api.models.registration_json_utils import cleanup_owner_groups, sort_owner_groups
from mhr_api.models.type_tables import MhrRegistrationStatusTypes, MhrOwnerStatusTypes
Expand Down Expand Up @@ -60,9 +59,10 @@ def post_transfers(mhr_number: str): # pylint: disable=too-many-return-statemen
return resource_utils.helpdesk_unauthorized_error_response('transfer of ownership')
request_json = request.get_json(silent=True)
current_app.logger.info(request_json)
group: str = get_group(jwt)
if not model_reg_utils.is_transfer_due_to_death(request_json.get('registrationType')) and \
not authorized_role(jwt, TRANSFER_SALE_BENEFICIARY):
current_app.logger.error('User not staff or missing required role: ' + TRANSFER_SALE_BENEFICIARY)
not authorized_role(jwt, TRANSFER_SALE_BENEFICIARY) and group != DEALERSHIP_GROUP:
current_app.logger.error('User not staff ({group}) or missing required role: ' + TRANSFER_SALE_BENEFICIARY)
return resource_utils.unauthorized_error_response(account_id)
if model_reg_utils.is_transfer_due_to_death(request_json.get('registrationType')) and \
not authorized_role(jwt, TRANSFER_DEATH_JT):
Expand All @@ -73,13 +73,11 @@ def post_transfers(mhr_number: str): # pylint: disable=too-many-return-statemen
current_reg: MhrRegistration = MhrRegistration.find_all_by_mhr_number(mhr_number,
account_id,
is_all_staff_account(account_id))
request_json = get_qs_dealer(request_json, group, account_id)
# Validate request against the schema.
valid_format, errors = schema_utils.validate(request_json, 'transfer', 'mhr')
# Additional validation not covered by the schema.
extra_validation_msg = resource_utils.validate_transfer(current_reg,
request_json,
is_staff(jwt),
get_group(jwt))
extra_validation_msg = resource_utils.validate_transfer(current_reg, request_json, is_staff(jwt), group)
if not valid_format or extra_validation_msg != '':
return resource_utils.validation_error_response(errors, reg_utils.VAL_ERROR, extra_validation_msg)
current_reg.current_view = True
Expand All @@ -90,7 +88,7 @@ def post_transfers(mhr_number: str): # pylint: disable=too-many-return-statemen
current_reg,
request_json,
account_id,
get_group(jwt),
group,
TransactionTypes.TRANSFER)
current_app.logger.debug(f'building transfer response json for {mhr_number}')
registration.change_registrations = current_reg.change_registrations
Expand Down Expand Up @@ -170,3 +168,13 @@ def setup_report(registration: MhrRegistration, # pylint: disable=too-many-loca
response_json['addOwnerGroups'] = response_add_groups
response_json['status'] = status
response_json = cleanup_owner_groups(response_json)


def get_qs_dealer(request_json: dict, group: str, account_id: str) -> dict:
"""Try to get the qualified supplier information by account id and add it to the request for validation."""
if not group or group != DEALERSHIP_GROUP:
return request_json
supplier = MhrQualifiedSupplier.find_by_account_id(account_id)
if supplier:
request_json['supplier'] = supplier.json
return request_json
22 changes: 22 additions & 0 deletions mhr_api/src/mhr_api/utils/registration_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@
'City and province may not be modified. '
PERMIT_ACTIVE_ACCOUNT_INVALID = 'Create new transport permit request invalid: an active, non-expired transport ' + \
'permit created by another account exists. '
TRANS_DEALER_DOC_TYPE_INVALID = 'QS dealers can only submit a TRANS transfer due to sale or gift. '
QS_DEALER_INVALID = 'No approved qualified supplier information found: supplier account set up invalid.'
DEALER_TRANSFER_OWNER_INVALID = 'QS dealer transfer invalid: either current owner group is not SOLE or the owner ' + \
'name does not match the qualified supplier account name. '

PPR_SECURITY_AGREEMENT = ' SA TA TG TM '

Expand Down Expand Up @@ -174,6 +178,7 @@ def validate_transfer(registration: MhrRegistration, # pylint: disable=too-many
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)
if reg_type != MhrRegistrationTypes.TRANS and json_data.get('transferDocumentType'):
error_msg += TRANS_DOC_TYPE_INVALID
elif not staff and json_data.get('transferDocumentType'):
Expand Down Expand Up @@ -720,3 +725,20 @@ def validate_active_permit(registration: MhrRegistration, account_id: str) -> st
if active_permit or (permit_account_id and account_id != permit_account_id):
error_msg += PERMIT_ACTIVE_ACCOUNT_INVALID
return error_msg


def validate_transfer_dealer(registration: MhrRegistration, json_data, reg_type: str, group: str):
"""Perform all extra transfer data validation checks for QS dealers."""
error_msg = ''
if not group or group != DEALERSHIP_GROUP:
return error_msg
if reg_type != MhrRegistrationTypes.TRANS or json_data.get('transferDocumentType'):
error_msg += TRANS_DEALER_DOC_TYPE_INVALID
if not json_data.get('supplier'):
error_msg += QS_DEALER_INVALID
return error_msg
if not validator_utils.is_valid_dealer_transfer_owner(registration, json_data.get('supplier')):
error_msg += DEALER_TRANSFER_OWNER_INVALID
if json_data.get('supplier'): # Added just for this validation.
del json_data['supplier']
return error_msg
65 changes: 45 additions & 20 deletions mhr_api/src/mhr_api/utils/validator_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,8 @@
from mhr_api.models import MhrRegistration, MhrDraft
from mhr_api.models import registration_utils as reg_utils, utils as model_utils, registration_json_utils
from mhr_api.models.type_tables import (
MhrDocumentTypes,
MhrLocationTypes,
MhrNoteStatusTypes,
MhrOwnerStatusTypes,
MhrPartyTypes,
MhrRegistrationStatusTypes,
MhrRegistrationTypes,
MhrStatusTypes,
MhrTenancyTypes
MhrDocumentTypes, MhrLocationTypes, MhrNoteStatusTypes, MhrOwnerStatusTypes, MhrPartyTypes,
MhrRegistrationStatusTypes, MhrRegistrationTypes, MhrStatusTypes, MhrTenancyTypes
)
from mhr_api.models.utils import is_legacy
from mhr_api.services import ltsa
Expand Down Expand Up @@ -246,11 +239,6 @@ def validate_registration_state_exemption(registration: MhrRegistration, reg_typ
error_msg += STATE_NOT_ALLOWED
elif reg_type == MhrRegistrationTypes.EXEMPTION_RES:
error_msg += EXEMPT_EXRS_INVALID
# elif registration.change_registrations:
# for reg in registration.change_registrations:
# if reg.registration_type == MhrRegistrationTypes.EXEMPTION_NON_RES and \
# reg.notes and reg.notes[0].status_type == MhrNoteStatusTypes.ACTIVE:
# error_msg += EXEMPT_EXNR_INVALID
return error_msg


Expand Down Expand Up @@ -403,15 +391,13 @@ def check_state_note(registration: MhrRegistration,
if registration.change_registrations:
for reg in registration.change_registrations:
if reg.notes:
if reg.notes[0].document_type in (MhrDocumentTypes.TAXN,
MhrDocumentTypes.NCON,
if reg.notes[0].document_type in (MhrDocumentTypes.TAXN, MhrDocumentTypes.NCON,
MhrDocumentTypes.REST) and \
reg.notes[0].status_type == MhrNoteStatusTypes.ACTIVE:
error_msg += STATE_FROZEN_NOTE
# STATE_FROZEN_PERMIT rule removed for QS residential exemptions 21424.
elif reg.registration_type in (MhrRegistrationTypes.PERMIT, MhrRegistrationTypes.PERMIT_EXTENSION) and \
reg_type not in (MhrRegistrationTypes.PERMIT,
MhrRegistrationTypes.PERMIT_EXTENSION,
reg_type not in (MhrRegistrationTypes.PERMIT, MhrRegistrationTypes.PERMIT_EXTENSION,
MhrRegistrationTypes.EXEMPTION_RES) and \
reg.notes[0].status_type == MhrNoteStatusTypes.ACTIVE and \
not model_utils.is_transfer(reg_type) and \
Expand All @@ -427,8 +413,7 @@ def check_state_cancel_permit(registration: MhrRegistration, error_msg: str) ->
return error_msg
if registration.change_registrations:
for reg in registration.change_registrations:
if reg.notes and reg.notes[0].document_type in (MhrDocumentTypes.EXRS,
MhrDocumentTypes.EXMN,
if reg.notes and reg.notes[0].document_type in (MhrDocumentTypes.EXRS, MhrDocumentTypes.EXMN,
MhrDocumentTypes.EXNR) and \
reg.notes[0].status_type == MhrNoteStatusTypes.ACTIVE:
error_msg += STATE_FROZEN_EXEMPT
Expand Down Expand Up @@ -529,6 +514,46 @@ def owner_name_match(registration: MhrRegistration = None, # pylint: disable=to
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()
current_app.logger.debug(f'is_valid_dealer_transfer_owner checking dealer name={qs_name} dba_name={dba_name}')
groups = []
if is_legacy():
groups = validator_utils_legacy.get_existing_owner_groups(registration)
else:
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:
current_app.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(registration: MhrRegistration = None, # pylint: disable=too-many-branches
json_data: dict = None) -> str:
"""Check groups id's and owners are valid for deleted groups."""
Expand Down
12 changes: 12 additions & 0 deletions mhr_api/src/mhr_api/utils/validator_utils_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,18 @@ def get_existing_group_count(registration) -> int:
return group_count


def get_existing_owner_groups(registration) -> dict:
"""Get the existing active/exempt owner groups."""
groups = []
if not registration or not registration.manuhome:
return groups
manuhome: Db2Manuhome = registration.manuhome
for existing in manuhome.reg_owner_groups:
if existing.status in (Db2Owngroup.StatusTypes.ACTIVE, Db2Owngroup.StatusTypes.EXEMPT):
groups.append(existing.json)
return groups


def check_state_note(manuhome: Db2Manuhome, staff: bool, error_msg: str, reg_type: str, doc_type: str = None) -> str:
"""Check registration state for non-staff: frozen if active TAXN, NCON, or REST unit note."""
if staff:
Expand Down
2 changes: 1 addition & 1 deletion mhr_api/src/mhr_api/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
Development release segment: .devN
"""

__version__ = '1.8.17' # pylint: disable=invalid-name
__version__ = '1.8.18' # pylint: disable=invalid-name
2 changes: 1 addition & 1 deletion mhr_api/tests/unit/api/test_registrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@
TEST_GET_BATCH_REGISTRATIONS = [
('Missing account', [MHR_ROLE], HTTPStatus.BAD_REQUEST, None, None, None),
('Invalid role', [COLIN_ROLE], HTTPStatus.UNAUTHORIZED, 'PS12345', None, None),
('Valid Request', [MHR_ROLE], HTTPStatus.OK, 'PS12345', '2023-12-15T08:01:00+00:00', '2023-12-22T08:01:00+00:00'),
('Valid Request', [MHR_ROLE], HTTPStatus.OK, 'PS12345', '2023-12-15T08:01:00%2B00:00', '2023-12-22T08:01:00%2B00:00'),
('Valid Default Request', [MHR_ROLE], HTTPStatus.OK, 'PS12345', None, None)
]
# testdata pattern is ({reg_type}, {trans_id})
Expand Down
6 changes: 4 additions & 2 deletions mhr_api/tests/unit/api/test_transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from mhr_api.models import MhrRegistration, MhrRegistrationReport, MhrDocument
from mhr_api.models.type_tables import MhrRegistrationTypes
from mhr_api.services.authz import BCOL_HELP_ROLE, MHR_ROLE, STAFF_ROLE, COLIN_ROLE, \
TRANSFER_DEATH_JT, TRANSFER_SALE_BENEFICIARY
TRANSFER_DEATH_JT, TRANSFER_SALE_BENEFICIARY, REQUEST_TRANSPORT_PERMIT
from tests.unit.services.utils import create_header, create_header_account
from tests.unit.utils.test_transfer_data import (
TRAND_DELETE_GROUPS,
Expand All @@ -42,6 +42,7 @@
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/'
DOC_ID_VALID = '63166035'
DEALER_ROLES = [MHR_ROLE,REQUEST_TRANSPORT_PERMIT]


# testdata pattern is ({description}, {mhr_num}, {roles}, {status}, {account})
Expand All @@ -62,7 +63,8 @@
('Invalid exempt', '000912', [MHR_ROLE, TRANSFER_SALE_BENEFICIARY], HTTPStatus.BAD_REQUEST, 'PS12345'),
('Invalid historical', '000913', [MHR_ROLE, TRANSFER_SALE_BENEFICIARY], HTTPStatus.BAD_REQUEST, 'PS12345'),
('Invalid non-staff missing declared value', '000900', [MHR_ROLE, TRANSFER_DEATH_JT, TRANSFER_SALE_BENEFICIARY],
HTTPStatus.BAD_REQUEST, 'PS12345')
HTTPStatus.BAD_REQUEST, 'PS12345'),
('Invalid dealer owner name', '000902', DEALER_ROLES, HTTPStatus.BAD_REQUEST, 'PS12345')
]
# testdata pattern is ({description}, {mhr_num}, {roles}, {status}, {account}, {reg_type})
TEST_CREATE_TRANS_DEATH_DATA = [
Expand Down
44 changes: 42 additions & 2 deletions mhr_api/tests/unit/utils/test_transfer_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
from registry_schemas.example_data.mhr import TRANSFER

from mhr_api.utils import registration_validator as validator, validator_utils
from mhr_api.models import MhrRegistration, utils as model_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
from mhr_api.services.authz import STAFF_ROLE, QUALIFIED_USER_GROUP
from mhr_api.services.authz import STAFF_ROLE, QUALIFIED_USER_GROUP, DEALERSHIP_GROUP
from tests.unit.utils.test_transfer_data import (
SO_OWNER_MULTIPLE,
SO_GROUP_MULTIPLE,
Expand Down Expand Up @@ -281,6 +281,14 @@
('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)
]
# testdata pattern is ({desc}, {valid}, {doc_type}, {account_id}, {mhr}, {has_qs_info} {message content})
TEST_DATA_DEALER = [
('Invalid no QS info', False, None, 'PS12345', '000915', False, validator.QS_DEALER_INVALID),
('Invalid doc type', False, 'TRANS_LAND_TITLE', 'PS12345', '000915', True, validator.TRANS_DEALER_DOC_TYPE_INVALID),
('Invalid existing owner SOLE', False, None, 'PS12345', '000915', True, validator.DEALER_TRANSFER_OWNER_INVALID),
('Invalid existing owner JOINT', False, None, 'PS12345', '000920', True, validator.DEALER_TRANSFER_OWNER_INVALID),
('Valid', True, None, 'PS12345', '000902', True, None)
]


@pytest.mark.parametrize('desc,valid,staff,doc_id,message_content,status', TEST_TRANSFER_DATA)
Expand Down Expand Up @@ -779,3 +787,35 @@ def test_validate_transfer_doc_type(session, desc, valid, doc_type, reg_type, me
assert error_msg != ''
if message_content:
assert error_msg.find(message_content) != -1


@pytest.mark.parametrize('desc,valid,doc_type,account_id,mhr_num,has_qs,message_content', TEST_DATA_DEALER)
def test_validate_dealer(session, desc, valid, doc_type, account_id, mhr_num, has_qs, message_content):
"""Assert that MH transfer QS dealer validation works as expected."""
# setup
json_data = copy.deepcopy(TRANSFER)
staff = False
group = DEALERSHIP_GROUP
json_data['registrationType'] = 'TRANS'
if doc_type:
json_data['transferDocumentType'] = doc_type
if has_qs:
supplier = MhrQualifiedSupplier.find_by_account_id(account_id)
if supplier:
json_data['supplier'] = supplier.json

valid_format, errors = schema_utils.validate(json_data, 'transfer', 'mhr')
# Additional validation not covered by the schema.
registration: MhrRegistration = MhrRegistration.find_all_by_mhr_number(mhr_num, account_id)
if valid:
registration.owner_groups[0].owners[0].business_name = json_data['supplier'].get('businessName')
error_msg = validator.validate_transfer(registration, json_data, staff, group)
# if errors:
# for err in errors:
# current_app.logger.debug(err)
if valid:
assert valid_format and error_msg == ''
else:
assert error_msg != ''
if message_content:
assert error_msg.find(message_content) != -1

0 comments on commit 3ec06ad

Please sign in to comment.