"""Logic for Vendor. Provides logic for getting vendor information. """ import uuid from datetime import datetime from time import time from typing import Dict, List, Optional, Union from connector_neo4j import get_session from ddtrace import tracer from flask import g from owsresponse import response from python_pdp_sdk import ( ResourceWithAttributes, ) from pythonfeatures.constants import split as split_constants # noqa from sqlalchemy.orm import Session from account import config from account.api import authorization_backend, kafka_producer from account.connectors.mysql import db_read_only_session_wrap from account.constants import connectors, constants, error, features from account.constants.constants import ( FETCH_TENANT_HIERARCHY, NULLABLE_STAFF_FIELDS, PRODUCT_MANAGER, STAFF_FIELDS, ) from account.models import ( country as country_model, feature as feature_model, neo4j_identity, neo4j_vendor, ows_royalties, types, vend_contact, vendor as vendor_model, ) from account.utils.api_utils import validate_country_code from account.utils.dataloader_util import format_for_dataloader from account.utils.exception import VendorUpdateException from account.utils.sanitize import sanitize_relationship_notes from account.utils.serialization import serialize_event def is_distributor(vendor_id): """Get whether vendor is a distributor. Args: vendor_id (int): unique identifier of the vendor. Returns: response.Response: success if vendor is a distributor. """ result = vendor_model.get_distributor(vendor_id) if result: return response.Response(True) return response.create_error_response( error.ERROR_CODE_NOT_FOUND, error.ERROR_MESSAGE_NO_MATCH, status=204 ) def is_vendor(vendor_id): """Get whether vendor_id is a valid vendor. Args: vendor_id (int): unique identifier of the vendor. Returns: response.Response: success if vendor_id is a valid vendor id. """ result = vendor_model.get_vendor(vendor_id) if not result: return result return response.Response(True) def profile_has_access(profile_id, profile_type, vendor_id): """Check neo4j for Profile access to Vendor Args: profile_id (int): id of profile checking for access profile_type (string): type of profile checking for access vendor_id (int): id of Vendor being requested access to Returns: True if Profile contains a has_access_to or has_admin_access_to relationship with vendor, False otherwise """ if not profile_id or not profile_type: return False return neo4j_vendor.profile_has_access_to_vendor(profile_id, profile_type, vendor_id) def profile_has_access_tx(profile_id, profile_type, vendor_id): """Check neo4j for Profile access to Vendor using a Neo4j Managed Transaction. Args: profile_id (int): id of profile checking for access profile_type (string): type of profile checking for access vendor_id (int): id of Vendor being requested access to Returns: True if Profile contains a has_access_to or has_admin_access_to relationship with vendor, False otherwise """ if not profile_id or not profile_type: return False session = get_session() return session.execute_read( neo4j_vendor.profile_has_access_to_vendor_tx, profile_id, profile_type, vendor_id ) def get_vendor_closers( vendor_uuids: list[str], ) -> List[Optional[Dict[str, Union[str, List[int]]]]]: """Get list of vendor closers. Args: vendor_uuids (list[str]): unique identifier of the vendor. Returns: List[Optional[Dict[str, Union[str, List[int]]]]]: vendor uuid and closers. """ result = vendor_model.get_vendor_closers(vendor_uuids) return format_for_dataloader( result, vendor_uuids, 'uuid', ) def get_vendors_first_statement_period( vendor_uuids: list[str], ) -> dict[str, list]: """Fetch the first statement period for each vendor UUID. Args: vendor_uuids (list[str]): List of vendor UUIDs. Returns: list[dict[str, str | int] | None]: List of dicts, each containing a UUID and its statement period. """ vendors = vendor_model.get_vendors_first_statement_period(vendor_uuids) return { 'vendors': format_for_dataloader( vendors, vendor_uuids, 'uuid', ) } def get_vendor_label_info(vendor_id, neo_tx=False): """Get vendor label info for OA. Args: vendor_id (int): unique identifier of the vendor. Returns: response.Response: (dict) vendor information from AR and Neo. """ vendor_response = vendor_model.get_vendor_label_info(vendor_id) if not vendor_response: return vendor_response vendor = vendor_response.message company = vendor.pop('company') company_brand_art_relations = vendor.pop('company_brand') vendor['account_name'] = None if not company else company name = vendor.pop('name') vendor['contact_name'] = None if not name else name sc_usa = vendor_model.get_soundscan_code(vendor_id, 1).message vendor['sc_usa'] = None if not sc_usa else sc_usa.get('soundscan_codes_soundscan_code') sc_ca = vendor_model.get_soundscan_code(vendor_id, 2).message vendor['sc_ca'] = None if not sc_ca else sc_ca.get('soundscan_codes_soundscan_code') recurring_payment_threshold = vendor_model.get_recurring_payment_threshold(vendor_id) vendor['recurring_payment_threshold'] = ( None if not recurring_payment_threshold else recurring_payment_threshold.message ) product_manager = vendor_model.get_product_manager_id(vendor_id).message vendor['product_manager'] = ( None if not product_manager else product_manager.get('product_manager') ) if neo_tx: session = get_session() neo4j_vendor_response = session.execute_read( neo4j_vendor.get_neo4j_vendor_info_tx, vendor_id ) else: neo4j_vendor_response = neo4j_vendor.get_neo4j_vendor_info(vendor_id) if not neo4j_vendor_response: return neo4j_vendor_response vendor.update(neo4j_vendor_response.message) vendor['company_brand'] = company_brand_art_relations return response.Response(vendor) def get_vendors(accessible_vendor_uuids: list[str], vendor_uuids: list[str]) -> dict[str, list]: """Get list of vendors label info. Args: accessible_vendor_uuids (list[str]): identifiers of the vendors which neo4j allowed for this identity. vendor_uuids (list[str]): identifiers of vendors in the request. Returns: dict[str, list]: vendor information from AR. """ result = vendor_model.get_vendors(accessible_vendor_uuids) vendors = [ {'uuid': item['vendor_uuid'], **{k: v for k, v in item.items() if k != 'vendor_uuid'}} for item in result if item is not None ] return { 'vendors': format_for_dataloader( vendors, vendor_uuids, 'uuid', ) } def get_vendor_names(vendor_uuids: list[str]) -> dict[str, list]: """Get list of vendor names. may raise if model raises. Args: vendor_uuids (list[str]): unique identifiers of the vendors. Returns: dict[str, list]: vendor names and identifiers from AR. """ result = vendor_model.get_vendor_names(vendor_uuids) vendors = [ {'uuid': item['vendor_uuid'], **{k: v for k, v in item.items() if k != 'vendor_uuid'}} for item in result if item is not None ] return { 'vendors': format_for_dataloader( vendors, vendor_uuids, 'uuid', ) } def get_vendor_company_brands( vendor_uuids: list[str], ) -> dict[str, list]: """Get list of vendor company_brands. may raise if model raises. Args: vendor_uuids (list[str]): unique identifiers of the vendors. Returns: dict[str, list]: vendor company_brands and identifiers from AR. """ vendors = vendor_model.get_vendor_company_brands(vendor_uuids) return { 'vendors': format_for_dataloader( vendors, vendor_uuids, 'uuid', ) } def get_vendor_service_tier(vendor_uuids: list[str]) -> dict[str, list]: """Get list of vendor service tiers. may raise if model raises. Args: vendor_uuids (list[str]): unique identifiers of the vendors. Returns: dict[str, list]: vendor service tiers and identifiers from AR. """ vendors = vendor_model.get_vendor_service_tier(vendor_uuids) return { 'vendors': format_for_dataloader( vendors, vendor_uuids, 'uuid', ) } def update_vendor(vendor_id, data): """Update existing vendor. Args: vendor_id (int): unique identifier for vendor. data (dict): supported fields for update. Returns: response.Response: vendor information. """ details = {} fields = { 'vendor_id': vendor_id, 'owner': data.get('owner'), 'is_owned': data.get('is_owned'), # Set name from data payload field 'name' if available, use 'contact_name' otherwise 'name': data.get('name') or data.get('contact_name'), 'company': data.get('name'), 'country_id': data.get('country_id'), 'assigned_to': data.get('assigned_to_id'), 'assigned_reviewer': data.get('assigned_reviewer_id'), 'primary_genre': data.get('primary_genre_id'), 'contact_email': data.get('contact_email'), 'est_total_releases': data.get('estimated_total_products'), 'est_total_tracks': data.get('estimated_total_tracks'), 'show_release_builder': data.get('show_release_builder'), 'priority': data.get('priority'), 'label_identifier': data.get('label_identifier'), 'label_summary': data.get('label_summary'), 'website': data.get('website'), 'support_contact_email': data.get('support_contact_email'), 'transfer_pricing_country': data.get('transfer_pricing_country'), 'region': data.get('region'), 'wel_email_sender': data.get('wel_email_sender'), 'wel_email_send_date': data.get('wel_email_send_date'), 'last_modified_by': data.get('last_modified_by'), } for key in fields: if fields[key] is not None or fields[key] == '': details.setdefault(key, fields[key]) if 'quarterback_label_manager_id' in data: details.setdefault('quarterback_label_manager', data.get('quarterback_label_manager_id')) try: ar_update = vendor_model.update_vendor(vendor_id, details) if not ar_update: return ar_update except Exception as e: return response.create_fatal_response(f'Unable to update vendor: {str(e)}') if data.get('company_brand_uuid'): company_brand_uuid = data.get('company_brand_uuid') try: vendor_model.update_company_brand_art_relations(vendor_id, company_brand_uuid) except Exception as e: return response.create_fatal_response( f'Unable to update company_brand_id:' f' Failed to update with company_brand_uuid' f' {company_brand_uuid} in mysql: {e.args[0]}' ) if data.get('service_tier_uuid'): try: neo4j_vendor.update_service_tier(vendor_id, data.get('service_tier_uuid')) except Exception: return response.create_fatal_response( f'Unable to update vendor: Failed to update Vendor {vendor_id} in Neo4j.' ) # if productManagerId is null, unmap product manager from label. if 'product_manager_id' in data: if data.get('product_manager_id') is not None: vendor_model.update_product_manager_id(vendor_id, data.get('product_manager_id')) else: vendor_model.delete_product_manager_id(vendor_id) if 'sc_usa' in data: sc_usa = data.get('sc_usa') try: if vendor_model.sc_map_exists(vendor_id, 1) != 0: vendor_model.update_soundscan_code(vendor_id, sc_usa, 1) else: vendor_model.map_soundscan_code(vendor_id, sc_usa, 1) except Exception as e: return response.create_fatal_response( f'Unable to update soundscan_code:' f' Failed to update soundscan_code to {sc_usa} in mysql: {e.args[0]}' ) if 'sc_ca' in data: sc_ca = data.get('sc_ca') try: if vendor_model.sc_map_exists(vendor_id, 2) != 0: vendor_model.update_soundscan_code(vendor_id, sc_ca, 2) else: vendor_model.map_soundscan_code(vendor_id, sc_ca, 2) except Exception as e: return response.create_fatal_response( f'Unable to update soundscan_code: ' f' Failed to update soundscan_code to {sc_ca} in mysql: {e.args[0]}' ) # retrieve updated vendor info, use get_vendor_label_info vendor = get_vendor_label_info(vendor_id).message update_vendor_kafka_event = {} for key in data.keys(): update_vendor_kafka_event.setdefault(key, data[key]) update_vendor_kafka_event.update({'vendor_id': vendor_id, 'vendor_uuid': vendor['vendor_uuid']}) publish_account_event(vendor_id, update_vendor_kafka_event, connectors.OperationType.UPDATE) return response.Response(vendor) def update_vendor_closers( vendor_uuid: str, closers: List[int], last_modified_by: Optional[int] = None ) -> types.UpdatedVendorClosers: """Update vendor closers using uuid and publish an account event. Raises VendorUpdateException on not-found or DB error. """ result = vendor_model.upsert_vendor_closers(vendor_uuid, closers, last_modified_by) publish_account_event( vendor_id=result['vendor_id'], response_payload=result, event_type=connectors.OperationType.UPDATE, ) return result def validate_first_statement_period(first_statement_period: int) -> int: """ Validates whether the given first_statement_period is not a future period. Args: first_statement_period (int): period_id. Returns: int: The same period ID if valid, or raises/returns an error response if invalid. """ period = ows_royalties.get_statement_period(first_statement_period) if not period: raise VendorUpdateException( code=error.ERROR_CODE_INVALID_REQUEST, message=f'No period found for period id : {first_statement_period}', status=400, ) today = datetime.today() if (period.get('statement_year') > today.year) or ( period.get('statement_year') == today.year and period.get('statement_month') > today.month ): raise VendorUpdateException( code=error.ERROR_CODE_INVALID_REQUEST, message=f'First statement period must not be a future date.' f' Received : {period.get("statement_month")}/{period.get("statement_year")}', status=400, ) return first_statement_period def update_vendor_first_statement_period( vendor_uuid: str, first_statement_period: int ) -> response.Response: """Update vendor first_statement_period using uuid""" try: first_statement_period = validate_first_statement_period(first_statement_period) return response.Response( vendor_model.update_vendor_first_statement_period(vendor_uuid, first_statement_period) ) except VendorUpdateException as e: g.log.exception(e.message) return response.create_error_response(code=e.code, message=e.message, status=e.status) except Exception as exc: g.log.exception(str(exc)) return response.create_fatal_response(f'Unable to update vendor: {str(exc)}') def update_vendor_notes( vendor_uuid: str, relationship_notes: str, ) -> response.Response: """Update the vendor's relationship notes using uuid. Args: vendor_uuid (str): Unique identifier for the vendor. relationship_notes (str): The relationship notes to update for the vendor. Returns: response.Response: Response object containing the updated vendor data or error details if the update fails. """ try: cleaned_notes = sanitize_relationship_notes(relationship_notes) return response.Response( vendor_model.update_vendor_relationship_notes(vendor_uuid, cleaned_notes) ) except VendorUpdateException as e: g.log.exception(e.message) return response.create_error_response(code=e.code, message=e.message, status=e.status) except Exception as exc: g.log.exception(str(exc)) return response.create_fatal_response(f'Unable to update vendor: {str(exc)}') def update_vendor_external_identifier_1( vendor_uuid: str, last_modified_by: int, external_identifier_1: str, ) -> types.UpdatedVendor: """Update the vendor's external_identifier_1. Raises VendorUpdateException on not-found or DB error. """ details = { 'external_identifier_1': external_identifier_1, 'last_modified_by': last_modified_by, } result = vendor_model.update_vendor_by_uuid(vendor_uuid, details) publish_account_event( vendor_id=result['vendor_id'], response_payload={ 'external_identifier_1': external_identifier_1, 'last_modified_by': last_modified_by, **result, }, event_type=connectors.OperationType.UPDATE, ) return result def update_vendor_country_id( vendor_uuid: str, last_modified_by: int, country_id: int, ) -> types.UpdatedVendor: """Update the vendor's country. Raises VendorUpdateException on not-found or DB error. """ details = { 'country_id': country_id, 'last_modified_by': last_modified_by, } result = vendor_model.update_vendor_by_uuid(vendor_uuid, details) publish_account_event( vendor_id=result['vendor_id'], response_payload={ 'country_id': country_id, 'last_modified_by': last_modified_by, **result, }, event_type=connectors.OperationType.UPDATE, ) return result def update_vendor_service_tier( vendor_uuid: str, last_modified_by: int, service_tier_uuid: str, ) -> types.UpdatedVendor: """Replace the vendor's service tier in art_relations. Neo4j is updated downstream by the existing art_relations -> neo4j sync. Raises VendorUpdateException on DB error. """ result = vendor_model.update_vendor_service_tier_by_uuid( vendor_uuid, service_tier_uuid, last_modified_by, ) publish_account_event( vendor_id=result['vendor_id'], response_payload={ 'service_tier_uuid': service_tier_uuid, 'last_modified_by': last_modified_by, **result, }, event_type=connectors.OperationType.UPDATE, ) return result def update_vendor_info( vendor_uuid: str, last_modified_by: int, **metadata_fields: Optional[str], ) -> types.UpdatedVendor: """Update vendor info (name, owner, company, support_contact_email, etc). Only the keys present in `metadata_fields` are written. `None` clears the corresponding nullable column for fields that allow it. Raises VendorUpdateException on DB error. """ details: dict = {**metadata_fields} details['last_modified_by'] = last_modified_by result = vendor_model.update_vendor_by_uuid(vendor_uuid, details) publish_account_event( vendor_id=result['vendor_id'], response_payload={**details, **result}, event_type=connectors.OperationType.UPDATE, ) return result def update_vendor_internal_staff( vendor_uuid: str, last_modified_by: int, **staff_fields: Optional[int], ) -> types.UpdatedVendor: """Update the vendor's internal staff assignments. Accepted keys are exactly `STAFF_FIELDS`: assigned_to, assigned_reviewer, quarterback_label_manager, wel_email_sender, product_manager. Unknown keys raise ValueError so typos don't silently no-op. Only the keys present in `staff_fields` are written. `None` is a valid value for the four nullable FKs in `NULLABLE_STAFF_FIELDS` and clears the column. product_manager is stored in product_manager_mapping_vendor rather than on the vendor row — it is upserted separately and cannot be unset here. Invalid orchadmin_users FKs surface as a DB constraint error. Raises VendorUpdateException on not-found or DB error. """ unknown_keys = set(staff_fields) - set(STAFF_FIELDS) if unknown_keys: raise ValueError(f'Unknown staff field(s): {sorted(unknown_keys)}') details: dict = {k: v for k, v in staff_fields.items() if k in NULLABLE_STAFF_FIELDS} details['last_modified_by'] = last_modified_by result = vendor_model.update_vendor_by_uuid(vendor_uuid, details) event_payload: dict = {**details, **result} if (product_manager := staff_fields.get(PRODUCT_MANAGER)) is not None: vendor_model.update_product_manager_id(result['vendor_id'], product_manager) event_payload[PRODUCT_MANAGER] = product_manager publish_account_event( vendor_id=result['vendor_id'], response_payload=event_payload, event_type=connectors.OperationType.UPDATE, ) return result def delete_vendor(vendor_uuid: str, last_modified_by: int) -> types.DeletedVendor: """Soft-delete a vendor by setting its status to 'deletion'. Publishes an account update event when this call performs the soft-delete. """ result = vendor_model.delete_vendor_by_uuid(vendor_uuid, last_modified_by) if result['status'] == 'deletion': publish_account_event( vendor_id=result['vendor_id'], response_payload=result, event_type=connectors.OperationType.UPDATE, ) return result def get_restricted_features_for_vendor(owner: Optional[str], company_brand_name: str) -> list[int]: """Determine restricted features based on owner and brand. 3rd Party Payee Accounts should only have accounting features, regardless of brand. Args: owner: The vendor's owner field company_brand_name: The company brand name Returns: List of feature IDs to restrict """ if owner == features.THIRD_PARTY_PAYEE_OWNER: return features.THIRD_PARTY_PAYEE_RESTRICTED_FEATURES return features.BRAND_TO_RESTRICTED_FEATURES.get(company_brand_name) def create_vendor(attrs: dict) -> response.Response: """Create a vendor in mysql and publish event.""" company_brand_name = attrs.get('company_brand') brand_response = vendor_model.get_company_brand_id_by_name( company_brand_name=company_brand_name, ) if brand_response.message: brand_id = brand_response.message.get('company_brand_id') else: # Will be a 404 response on not finding a brand with this name return brand_response user_id = attrs.get('user_id') # default is_distributor to 'N' is_distributor_attr = 'N' if attrs.get('is_distributor', False) is True: is_distributor_attr = 'Y' label_identifier = None if is_distributor_attr == 'Y': label_identifier = 'D3' country_code = attrs.get('country') country_id = None if country_code: country_id = country_model.get_country_id_by_code(country_code) label_summary = attrs.get('label_summary') ar_attributes = { 'name': attrs.get('name'), 'owner': attrs.get('owner'), 'company_brand_id': brand_id, 'migrated_to_abacus': True, # all accounts under all brands are migrated to Abacus. 'last_modified_by': user_id, 'is_distributor': is_distributor_attr, 'label_identifier': label_identifier, 'country_id': country_id, 'primary_genre': attrs.get('primary_genre'), 'label_summary': label_summary, 'assigned_to': attrs.get('assigned_to'), 'assigned_reviewer': attrs.get('assigned_reviewer'), 'quarterback_label_manager': attrs.get('quarterback_label_manager'), 'wel_email_sender': attrs.get('wel_email_sender'), } # create Vendor with service tier response_obj = vendor_model.create_vendor_with_optional_service_tier( details=ar_attributes, service_tier_uuid=attrs['service_tier_uuid'] ) if response_obj.status >= 400: return response_obj # insert restricted features for vendor vendor_id = response_obj.message.get('vendor_id') feature_model.bulk_add_restricted_features_for_vendor( vendor_id=vendor_id, feature_ids=get_restricted_features_for_vendor(attrs.get('owner'), company_brand_name), ) vendor_uuid = response_obj.message.get('vendor_uuid') closers = attrs.get('closers') if company_brand_name == 'awal' and not closers: # Default AWAL closer until setting closer id via A360 is supported. closers = [constants.DEFAULT_AWAL_CLOSER_ID] if closers: try: update_vendor_closers(vendor_uuid, closers, user_id) except VendorUpdateException as e: return response.create_error_response(code=e.code, message=e.message, status=e.status) pm_id = attrs.get('product_manager') event_payload = {**response_obj.message, 'payment_currency': attrs.get('payment_currency')} if pm_id is not None: vendor_model.update_product_manager_id(vendor_id, pm_id) event_payload['product_manager'] = pm_id publish_account_event( # This param isn't actually used by publish_account_event vendor_id=event_payload['vendor_id'], response_payload=event_payload, event_type=connectors.OperationType.CREATE, ) return response_obj def create_or_update_vendor(data): """v1 Create a new vendor or update an existing vendor. Returns: response.Response: vendor information. """ owner = data.get('owner') existing_vendor_id = data.get('vendor_id') details = { 'name': data.get('vendor_name'), 'company': data.get('vendor_name'), 'contact_email': data.get('email'), 'owner': owner, 'label_identifier': data.get('label_identifier'), 'status': data.get('status'), 'migrated_to_abacus': data.get('migrated_to_abacus', False), 'assigned_to': data.get('assigned_to'), 'assigned_reviewer': data.get('assigned_reviewer'), 'quarterback_label_manager': data.get('quarterback_label_manager'), 'external_identifier_1': data.get('external_identifier_1'), 'is_distributor': data.get('is_distributor'), } if company_brand_name := data.get('company_brand'): result = vendor_model.get_company_brand_id_by_name(company_brand_name) company_brand_id = result.message.get('company_brand_id') if company_brand_id: details['company_brand_id'] = company_brand_id if not existing_vendor_id: # TODO: move create logic to a POST handler service_tier_uuid = data.get('service_tier_uuid') ar_result = vendor_model.create_vendor_with_optional_service_tier( details, service_tier_uuid=service_tier_uuid ) if not ar_result: return ar_result # Handle closer_id if provided for creates closer_id = data.get('closer_id') if closer_id is not None: vendor_uuid = ar_result.message.get('vendor_uuid') if vendor_uuid: try: update_vendor_closers(vendor_uuid, [closer_id]) except VendorUpdateException as e: return response.create_error_response( code=e.code, message=e.message, status=e.status ) else: ar_update = vendor_model.update_vendor(existing_vendor_id, details) if not ar_update: return ar_update ar_result = vendor_model.get_vendor(existing_vendor_id) # catch SQLErrors before attempting neo4j transactions. if not ar_result: return ar_result vendor_id = ar_result.message.get('vendor_id') # validate iso alpha 3 country code to be added to vendor in neo4j if data.get('country'): try: ar_result.message['country'] = validate_country_code(data.get('country')) except Exception as e: return response.create_error_response( error.ERROR_CODE_INVALID_INPUT, str(e), status=422 ) else: # if no country code is passed, set prop to null. ar_result.message['country'] = None ar_result.message['source'] = data.get('source') if neo4j_vendor.get_vendor(vendor_id): ar_result.message = _update_neo4j_vendor(data, ar_result.message) else: ar_result.message = _create_neo4j_vendor(data, ar_result.message) publish_account_event( ar_result.message['vendor_id'], ar_result.message, connectors.OperationType.CREATE, ) return ar_result def _update_neo4j_vendor(data: dict, ar_result: dict) -> Union[dict, response.Response]: """v1 Updates an existing vendor in neo4j given details. # TODO: reorganize with PLATFORM-3321 create/update split """ vendor_id = ar_result.get('vendor_id') try: neo4j_vendor.create_or_update_vendor(ar_result) except Exception: return response.create_fatal_response(f'Could not update vendor {vendor_id} in neo4j.') if data.get('service_tier_uuid'): neo4j_vendor.update_service_tier(vendor_id, data.get('service_tier_uuid')) ar_result['service_tier_uuid'] = data.get('service_tier_uuid') return ar_result def _create_neo4j_vendor(data: dict, ar_result: dict) -> Union[dict, response.Response]: """v1 Creates a vendor in neo4j given details. # TODO: reorganize with PLATFORM-3321 create/update split """ vendor_id = ar_result.get('vendor_id') try: neo4j_vendor.create_or_update_vendor(ar_result) except Exception: message = 'Could not create new vendor in neo4j.' g.ows.log.error(message) return response.create_fatal_response(message) if data.get('service_tier_uuid'): neo4j_vendor.add_service_tier(vendor_id, data.get('service_tier_uuid')) ar_result['service_tier_uuid'] = data.get('service_tier_uuid') return ar_result def get_vendor_document(vendor_id, with_tenant_uuids=False): """Get the vendor document for cloudsearch corpus. Args: vendor_id (int): id of vendor with_tenant_uuids (bool): Include 3 tenant level UUID fields in result or not. Vendor wont have subaccountUUID. Returns: response.Response: vendor info. """ results = vendor_model.get_vendor_document_by_id(vendor_id, with_tenant_uuids) if not results: return results return response.Response(results.message) def get_all_vendor_currency_codes(): """Get all currency codes for all vendors. Returns: response.Response: all currency codes for all vendors. """ return vendor_model.get_all_vendor_currency_codes() def get_vendor_service_details(vendor_id): """Get service tier for a specific vendor. Returns: response.Response: service tier or None. """ return neo4j_vendor.get_vendor_service_tier(vendor_id=vendor_id) def get_vendor_assigned_to(vendor_id): """Get assigned-to for a specific vendor.""" return vendor_model.get_assigned_to_by_id(vendor_id) def get_vendor_assigned_reviewer(vendor_id): """Get assigned-reviewer for a specific vendor.""" return vendor_model.get_assigned_reviewer_by_id(vendor_id) def get_vendor_secondary_internal_contact(vendor_id): """Get secondary internal contact for a specific vendor.""" return vendor_model.get_secondary_internal_contact_by_id(vendor_id) def update_is_distributor_in_vendor(vendor_id): """Update is_distributor field in vendor table Args: vendor_id (int): Vendor id. Returns: response.Response: vendor information. """ return vendor_model.update_is_distributor_in_vendor(vendor_id) def get_vendor_company_brand(vendor_id): """Get company brand for a specific vendor.""" if vendor_id is None: return response.create_error_response( error.ERROR_CODE_INVALID_REQUEST, error.ERROR_MESSAGE_INVALID_VENDOR_ID ) try: vendor_id = int(vendor_id) except ValueError: return response.create_error_response( code=error.ERROR_CODE_INVALID_REQUEST, message=error.ERROR_MESSAGE_INVALID_VENDOR_ID, status=400, ) return neo4j_vendor.get_company_brand(vendor_id) def get_vendor_company_brand_tx(vendor_id): """Get company brand for a specific vendor using a Neo4j Managed Transaction. Args: vendor_id (int): Vendor ID. """ if vendor_id is None: return response.create_error_response( error.ERROR_CODE_INVALID_REQUEST, error.ERROR_MESSAGE_INVALID_VENDOR_ID ) try: vendor_id = int(vendor_id) except ValueError: return response.create_error_response( code=error.ERROR_CODE_INVALID_REQUEST, message=error.ERROR_MESSAGE_INVALID_VENDOR_ID, status=400, ) session = get_session() return session.execute_read(neo4j_vendor.get_company_brand_tx, vendor_id) def publish_account_event(vendor_id, response_payload, event_type): """Publish an account event to kafka topic.""" event_payload = { 'operation': {'type': event_type, 'timestamp': time() * 1000}, 'payload': serialize_event(response_payload), } # This writes to the topic synchronously, since auto_flush defaults to True kafka_producer.produce( config.KAFKA_OWS_ACCOUNT_ACCOUNTS_TOPIC, str(uuid.uuid4()), event_payload, publish_account_event_callback, ) def publish_account_event_callback(error, message): """Log account event. Args: error: defaults to None message (cimpl.Message): The published event """ if error is not None: g.ows.log.error(error) g.ows.log.info(message) def get_service_tiers(): """Get list of service tiers.""" return neo4j_vendor.get_service_tiers() def lookup_vendors_by_uuids( uuids: list[str], fetch_flags: list[str] | None = None, ) -> response.Response: """Lookup vendors using uuids. NOTE: This function is configured for an endpoint which requires NO access rule checks. This is because it is serving as a Policy Information Point for Permission Platform. Do not expose other properties or attributes via this endpoint. Args: uuids: list of uuid strings. fetch_flags: list of fetch flags to be used in the lookup. """ if fetch_flags is None: fetch_flags = [] uuids = [str(uuid) for uuid in uuids] result = vendor_model.lookup_vendors_by_uuids(uuids, fetch_flags) if result: result.message = { 'vendors': format_for_dataloader( result.message, uuids, 'uuid', ) } return result def lookup_vendors_by_vendor_ids( vendor_ids: list, fetch_flags: list[str] | None = None, ): """Lookup vendors using vendor_ids. NOTE: This function is configured for an endpoint which requires NO access rule checks. This is because it is serving as a Policy Information Point for Permission Platform. Do not expose other properties or attributes via this endpoint. Args: vendor_ids: list of vendor ids. fetch_flags: list of fetch flags to be used in the lookup. """ if fetch_flags is None: fetch_flags = [] result = vendor_model.lookup_vendors_by_vendor_ids(vendor_ids, fetch_flags) if result: result.message = { 'vendors': format_for_dataloader( result.message, vendor_ids, 'vendor_id', ) } return result def get_vendors_by_external_identifier(external_identifier_1, owner): """Get all vendors by external_identifier_1 and owner. Args: external_identifier_1 (str): external_identifier_1. owner (str): Vendor owner. Returns: response.Response: containing dict of vendor's ids. """ return vendor_model.get_vendors_by_external_identifier(external_identifier_1, owner) @db_read_only_session_wrap def get_master_contact(vendor_id: int, session: Session) -> response.Response: """Get master contact for a given vendor.""" master_contact = vend_contact.VendContact.get_master_contact(tx=session, vendor_id=vendor_id) if not master_contact: return response.create_not_found_response(message='Master contact not found.') if len(master_contact) == 1: auth0_user_id = master_contact[0].auth0_user_id identity_id = neo4j_identity.get_identity_by_auth0_user_id(auth0_user_id) # Will occur if the master contact user never migrated over to auth0 if not identity_id: return response.create_not_found_response( message=f'Identity not found for auth0_user_id: {auth0_user_id}.' ) return response.Response( types.MasterContact( identity_id=identity_id, ) ) return response.create_error_response( code=error.ERROR_CODE_MASTER_CONTACT_ERROR, message=error.ERROR_MESSAGE_INVALID_MASTER_CONTACT, ) @tracer.wrap() def get_pp_accessible_vendors(vendor_uuids: list[str]) -> list[str]: """Authorize vendors using pdp authorization_backend. Args: vendor_uuids (str): list of vendor uuids. Returns: list[str]: filtered list of vendor_uuids based on access. """ vendors_with_company_brand_response = lookup_vendors_by_uuids( vendor_uuids, fetch_flags=[FETCH_TENANT_HIERARCHY] ) vendors_with_company_brand = vendors_with_company_brand_response.message['vendors'] resources_with_attributes = [ ResourceWithAttributes( resource_id=v['uuid'], attributes={ 'tenant': { 'tenant_type': 'account', 'tenant_uuid': v['uuid'], 'tenant_hierarchy': [v['company_brand_uuid']], } }, ) for v in vendors_with_company_brand ] auth_response = authorization_backend.is_authorized_many( action='view_account_info', resource_type='account', resources_with_attributes=resources_with_attributes, ) if len(auth_response) != len(vendor_uuids): raise ValueError( f'Authorization response contained {len(auth_response)} results for ' f'{len(vendor_uuids)} vendors.' ) accessible_vendor_uuids = [ resource.resource_id for resource, is_authorized in zip(resources_with_attributes, auth_response) if is_authorized ] return accessible_vendor_uuids def get_vendors_relationship_notes( vendor_uuids: list[str], ) -> dict[str, list]: """ Fetch relationship notes for each vendor UUID. """ notes = vendor_model.get_relationship_notes(vendor_uuids) return { 'vendors': format_for_dataloader( notes, vendor_uuids, 'vendor_uuid', ) }