"""Shared validation logic.""" from collections import namedtuple import logging from abacus_common_data.country import Country from flask import request from marshmallow import ValidationError from owsrequest import flask_request from owsrequest.context import get_request_context_from_headers from requests.exceptions import RequestException from payee.config import ows_client from payee.constants import error from payee.constants.constants import ( COLLABORATOR_RESOURCE_NAME, EMPLOYEE_ALL_VENDORS_MARK, LABEL_RESOURCE_NAME, ORCHARD_PAYEE_BYPASS_VAT_VALIDATION_ROLE, ORCHARD_PROFILE_ID, ORCHARD_PROFILE_TYPE, ORCHARD_ROLES, OWS_ABACUS_ACCOUNT_ENDPOINT_URL, OWS_ABACUS_ACCOUNT_PAYEE_DATALOADER, OWS_ABACUS_ACCOUNT_SERVICE_NAME, OWS_PERMISSIONS_ACCOUNT_ENDPOINT_URL, OWS_PERMISSIONS_SERVICE_NAME, VENDOR_RESOURCE_NAME, ) from payee.logic.graphql import get_payee_country_of_tax_residency import payee.logic.tax_details as tax_details from payee.models.account_payee import AccountPayee logger = logging.getLogger('payee') ResourceAccessCheck = namedtuple('ResourceAccessCheck', ['type', 'ids']) def validate_country_code(country_code): """Validate a country code parameter.""" try: Country(country_code) except KeyError: raise ValidationError(error.ERROR_UNKNOWN_COUNTRY.format(code=country_code)) def get_profile_vendors(profile_type, profile_id): """Get vendors info from ows-permissions microservice for a given profile.""" return ows_client.get( OWS_PERMISSIONS_SERVICE_NAME, OWS_PERMISSIONS_ACCOUNT_ENDPOINT_URL.format( profile_type=profile_type, profile_id=profile_id, resource_type=LABEL_RESOURCE_NAME, ), ) def get_profile_collaborators(profile_type, profile_id): """Get collaborators info from ows-permissions microservice for a given profile.""" return ows_client.get( OWS_PERMISSIONS_SERVICE_NAME, OWS_PERMISSIONS_ACCOUNT_ENDPOINT_URL.format( profile_type=profile_type, profile_id=profile_id, resource_type=COLLABORATOR_RESOURCE_NAME, ), ) def get_ows_headers(profile_type, profile_id): """Get ows profile headers.""" return { ORCHARD_PROFILE_TYPE: profile_type, ORCHARD_PROFILE_ID: profile_id, ORCHARD_ROLES: request.headers.get(ORCHARD_ROLES), } def get_accounts_by_account_payee_ids(account_payee_ids, headers: dict): """Get abacus accounts by account payee ids. Args: account_payee_ids (list): Abacus Account Payee Ids headers (dict): headers dict Return: accounts (dicts|None): Abacus Accounts """ ows_abacus_account_response = ows_client.post( OWS_ABACUS_ACCOUNT_SERVICE_NAME, OWS_ABACUS_ACCOUNT_PAYEE_DATALOADER, headers=headers, json=account_payee_ids, ) if ows_abacus_account_response.status_code == 200: data = ows_abacus_account_response.json()['items'] return data return None def get_account_id_by_account_payee_id(account_payee_id, headers: dict): """Get abacus account_id by account payee_id. Args: account_payee_id (int): Abacus Account Payee Id headers (dict): headers dict Return: account_id (int|None): Abacus Account Id """ ows_abacus_account_response = ows_client.get( OWS_ABACUS_ACCOUNT_SERVICE_NAME, OWS_ABACUS_ACCOUNT_ENDPOINT_URL.format(account_payee_id=account_payee_id), headers=headers, ) if ows_abacus_account_response.status_code == 200: account_id = ows_abacus_account_response.json()['account_id'] return account_id return None def validate_country_of_tax_residence_is_set(account_payee_id: int) -> bool: """Validate country of tax residence is set for a given payee.""" country_code = get_payee_country_of_tax_residency(account_payee_id) if country_code: return True return False def validate_vat_collected(account_payee_id: int) -> bool: """Validate vat is set for a given payee.""" payee_tax_details = tax_details.has_tax_details(account_payee_id) if payee_tax_details: return True return False def validate_w_form_exists(account_payee_id: int) -> bool: """ Validate vat is set for a given payee. Deprecated and will be removed, so just return `False` to prevent it erroring out the new users. """ return False def validate_multiple_record_owner(account_payee_ids): """Validate if request was made by the owner of the account payees records.""" false_result = {account_payee_id: False for account_payee_id in account_payee_ids} profile_type, profile_id = flask_request.get_profile_headers(request) if not profile_id or not profile_type: return false_result accounts_data = AccountPayee.get_payees_by_ids(account_payee_ids) if len(accounts_data) < 1: return false_result mapped_payee_account = { payee.account_payee_id: payee.account_id for payee in accounts_data } ows_permission_response = get_profile_vendors(profile_type, profile_id) if ows_permission_response.status_code == 200: response_data = ows_permission_response.json() if response_data['items']: vendors = [] for item in response_data['items']: # We need to check only type = `Vendor` if item.get('type') == VENDOR_RESOURCE_NAME: if item.get('vendorId') == EMPLOYEE_ALL_VENDORS_MARK: return { account_payee_id: True for account_payee_id in account_payee_ids } vendors.append(item.get('vendorId')) return { account_payee_id: mapped_payee_account[account_payee_id] in vendors if account_payee_id in mapped_payee_account else False for account_payee_id in account_payee_ids } return false_result def _get_vendor_ids(profile_type, profile_id) -> list[int]: try: response = get_profile_vendors(profile_type, profile_id) response.raise_for_status() vendor_items = response.json().get('items', []) except (RequestException, ValueError) as e: logger.warning( f'Error getting profile vendors for {profile_type}/{profile_id}: {e}' ) return [] return [ item['vendorId'] for item in vendor_items if item.get('type') == VENDOR_RESOURCE_NAME and isinstance(item['vendorId'], int) ] def _get_collaborator_ids(profile_type, profile_id) -> list[int]: try: response = get_profile_collaborators(profile_type, profile_id) response.raise_for_status() collaborator_items = response.json().get('items', []) except (RequestException, ValueError) as e: logger.warning( f'Error getting profile vendors for {profile_type}/{profile_id}: {e}' ) return [] return [item['id'] for item in collaborator_items if isinstance(item['id'], int)] def validate_record_owner( resource_access_check: ResourceAccessCheck, ) -> bool: """Validate if request was made by the owner of the payees records. Args: request_ids (ExtractedRequestIds): Helper object containing IDs and type of the resources to check Returns: bool: True if permission exists, False otherwise """ if not resource_access_check or not resource_access_check.ids: return False profile_type, profile_id = flask_request.get_profile_headers(request) if not (profile_type and profile_id): return False resource_ids = [] if resource_access_check.type == LABEL_RESOURCE_NAME: resource_ids = _get_vendor_ids(profile_type, profile_id) elif resource_access_check.type == COLLABORATOR_RESOURCE_NAME: resource_ids = _get_collaborator_ids(profile_type, profile_id) if not resource_ids and len(resource_access_check.ids): return False return set(resource_access_check.ids).issubset(set(resource_ids)) def validate_vat_bypass_validation_role(request): """Check if user has appropriate role for bypass vat validation.""" context = get_request_context_from_headers(request.headers) return ORCHARD_PAYEE_BYPASS_VAT_VALIDATION_ROLE in context.roles