"""Vendor related handlers.""" from connector_neo4j import Neo4jSession from ddtrace import tracer from flask import Response, g, request from owsrequest import error_response, flask_request from owsrequest.constants import headers as header_constants from owsresponse import response from owsresponse.adaptors.flask import flaskify from pythonfeatures import pythonfeatures from pythonfeatures.constants import split as split_constants from account import config from account.api import app, authorization_backend from account.constants import error, header from account.constants.connectors import NEO4J_DATABASE_NAME from account.constants.constants import ( FEATURE_USE_NEO4J_MANAGED_TX, STAFF_FIELDS, WARNING_MESSAGE_VENDOR_ACCESS_CHECK, ) from account.logic import feature, identity as identity_logic, sme, subaccount, supplier, vendor from account.logic.resource_getters.account import ( AccountByIdResourceGetter, AccountByUuidResourceGetter, ) from account.logic.resource_getters.company_brand import CompanyBrandByNameResourceGetter from account.models import feature as feature_model, neo4j_vendor, ows_permissions from account.utils import pagination from account.utils.api_utils import jwt_check, validate_request_data from account.utils.exception import VendorUpdateException from account.validation import validation from account.validation.schemas import create_vendor from account.validation.schemas.dataloader import ( VendorClosersRequestSchema, VendorCompanyBrandsDataloaderSchema, VendorFirstStatementPeriodRequestSchema, VendorRelationshipNotesRequestSchema, VendorServiceTierDataloaderSchema, ) from account.validation.schemas.update_vendor import ( UpdateVendorClosersSchema, UpdateVendorCountryIdSchema, UpdateVendorExternalIdentifier1Schema, UpdateVendorFirstStatementPeriodSchema, UpdateVendorInfoSchema, UpdateVendorInternalStaffSchema, UpdateVendorNotesSchema, UpdateVendorSchema, UpdateVendorServiceTierSchema, ) from account.validation.schemas.vendor_restricted_features import ( BulkAddVendorRestrictedFeaturesSchema, BulkRemoveVendorRestrictedFeaturesSchema, ) @app.route('/vendor/', methods=['HEAD']) def is_valid_vendor(vendor_id): """Validate if vendor_id is a valid vendor. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(vendor.is_vendor(vendor_id)) @app.route('//distributor', methods=['HEAD']) def is_vendor_a_distributor(vendor_id): """Validate vendor is a distributor. Given vendor with , gets if vendor is a distributor. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) validation_response = flask_request.verify_grass_access( request, required=False, vendor=vendor_id ) if not validation_response: return flaskify(response=validation_response) data = vendor.is_distributor(vendor_id) return flaskify(data) @app.route('/sony/vendor/', methods=['HEAD']) def is_vendor_sme(vendor_id): """Validate vendor is Sony distributed. Given vendor with , returns status code 200 if vendor is Sony distributed, otherwise returns 404. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) validation_response = flask_request.verify_grass_access( request, required=False, vendor=vendor_id ) if not validation_response: return flaskify(validation_response) data = sme.is_sme_vendor(vendor_id) return flaskify(data) @app.route('/vendor/', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_vendor(vendor_id): """Return basic information about vendor. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) pdp_check = False if not access_rule_decision: pdp_check = authorization_backend.is_authorized( action='view', resource_id=vendor_id, resource_type='account', resource_getter=AccountByIdResourceGetter(vendor_id), ) if not pdp_check: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) use_managed_tx = ( pythonfeatures.get_single_feature(FEATURE_USE_NEO4J_MANAGED_TX, g.request_context) ).message == split_constants.FEATURE_ENABLED if not pdp_check: profile_vendor_access_call = ( vendor.profile_has_access_tx if use_managed_tx else vendor.profile_has_access ) # Grass-Account-Type and Grass-Id are not required, but if they are passed # then they are verified grass_account_type, grass_id = flask_request.get_grass_headers(request) validation_response = response.Response() if grass_account_type == header.GRASS_ACCOUNT_TYPE_SUBACCOUNT: if not subaccount.is_subaccount_for_vendor(grass_id, vendor_id): validation_response = response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=error.ERROR_MESSAGE_FORBIDDEN_USER, status=403, ) else: validation_response = flask_request.verify_grass_access( request, required=False, vendor=vendor_id ) if not validation_response: return flaskify(validation_response) request_profile_id = g.request_context.profile_id request_profile_type = g.request_context.profile_type if config.ENVIRONMENT == config.QA_ENVIRONMENT and ( request_profile_id is None and request_profile_type is None ): pass # Suppress warnings due to curl requests in QA that omit headers elif not profile_vendor_access_call(request_profile_id, request_profile_type, vendor_id): g.log.warning( WARNING_MESSAGE_VENDOR_ACCESS_CHECK.format( request_profile_id, request_profile_type, vendor_id ) ) if use_managed_tx: return flaskify(vendor.get_vendor_label_info(vendor_id, neo_tx=True)) return flaskify(vendor.get_vendor_label_info(vendor_id)) @app.route('/vendors/first_statement_period/dataloader', methods=['POST']) @validate_request_data(VendorFirstStatementPeriodRequestSchema()) def get_vendors_first_statement_period(deserialize_schema): """Handler to get first statement periods for vendors in bulk. Expects JSON payload with a list of vendor uuids. Example payload: {"vendor_uuids": ['valid uuid']} Returns: Flask.response. """ vendor_uuids = deserialize_schema['vendor_uuids'] access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) try: return flaskify(response.Response(vendor.get_vendors_first_statement_period(vendor_uuids))) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/vendors/closers/dataloader', methods=['POST']) @validate_request_data(VendorClosersRequestSchema()) def get_vendor_closers(deserialize_schema): """Handler to get bulk vendor closers. Expects JSON payload with a list of vendor uuids. Example payload: {"vendor_uuids": ['valid uuid']} Returns: Flask.response. """ vendor_uuids = deserialize_schema['vendor_uuids'] access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) try: return flaskify(response.Response(vendor.get_vendor_closers(vendor_uuids))) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/vendors/dataloader', methods=['POST']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_vendors() -> Response: """Handler to get bulk vendors Expects JSON payload with a list of vendor uuids. Example payload: ['uuid1', 'uuid2'] Returns: Flask.response. """ vendor_uuids = request.get_json() if not isinstance(vendor_uuids, list) or not all( isinstance(item, str) for item in vendor_uuids ): return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_REQUEST ) ) # temporary logging to understand the behavior of the clients headers = request.headers.copy() authorization_header = next( (key for key in headers.keys() if key.lower() == 'authorization'), None ) if authorization_header: headers.pop(authorization_header, None) identity_id = g.request_context.identity_id profile_id = g.request_context.profile_id profile_type = g.request_context.profile_type # for oa / alw users, the check will work as these legacy headers # get converted to profile headers. access_rule_decision = flask_request.verify_rules_access_standalone(request) try: if not access_rule_decision: identity_uuid = g.request_context.jwt_identity_id if not identity_uuid: return flaskify( response.create_error_response( error.ERROR_CODE_AUTHORIZATION, message='Request context has no identity uuid.', status=401, ) ) # Get accessible vendors through pdp auth backend. accessible_vendor_uuids = vendor.get_pp_accessible_vendors(vendor_uuids=vendor_uuids) else: # Get accessible vendors through neo4j accessible_vendor_uuids = neo4j_vendor.accessible_vendors( profile_id=profile_id, profile_type=profile_type, vendor_uuids=vendor_uuids, identity_id=identity_id, ) # Log warning if any vendors are inaccessible if len(accessible_vendor_uuids) != len(vendor_uuids): g.ows.log.warn( 'vendor-dataloader: called to access inaccessible vendor', resources={ 'profile': { 'profile_id': profile_id, 'profile_type': profile_type, 'identity_id': identity_id, }, 'headers': { 'profile_id': headers.get('Orchard-Profile-Id'), 'profile_type': headers.get('Orchard-Profile-Type'), 'identity_id': headers.get('Orchard-Identity-Id'), 'user_id': headers.get('Orchard-User-Id'), }, 'vendors': set(vendor_uuids) - set(accessible_vendor_uuids), 'input_vendors': vendor_uuids, 'accessible_vendors': accessible_vendor_uuids, }, ) return flaskify( response.Response( vendor.get_vendors( accessible_vendor_uuids=accessible_vendor_uuids, vendor_uuids=vendor_uuids ) ) ) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/vendors/names/dataloader', methods=['POST']) def get_vendors_names() -> Response: """Handler to get vendor names in bulk Expects JSON payload with a list of vendor uuids. Example payload: ['uuid1', 'uuid2'] Returns: Flask.response. """ vendor_uuids = request.get_json() if not isinstance(vendor_uuids, list) or not all( isinstance(item, str) for item in vendor_uuids ): return flaskify( response.create_error_response( error.ERROR_CODE_INVALID_INPUT, error.ERROR_MESSAGE_INVALID_REQUEST ) ) try: return flaskify(response.Response(vendor.get_vendor_names(vendor_uuids))) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/vendors/company_brands/dataloader', methods=['POST']) @validate_request_data(VendorCompanyBrandsDataloaderSchema()) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_vendors_company_brands(deserialize_schema) -> Response: """Handler to get vendor company_brands in bulk Expects JSON payload with vendor_uuids list and optional skip_access_check flag. The skip_access_check flag is a temporary parameter that mimics an existing flag in GraphQL for backwards compatibility. Example payload: {"vendor_uuids": ["uuid1", "uuid2"], "skip_access_check": false} Returns: Flask.response. """ vendor_uuids = deserialize_schema['vendor_uuids'] try: return flaskify(response.Response(vendor.get_vendor_company_brands(vendor_uuids))) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/vendors/service_tier/dataloader', methods=['POST']) @validate_request_data(VendorServiceTierDataloaderSchema()) def get_vendors_service_tier(deserialize_schema) -> Response: """Handler to get vendor service tiers in bulk Expects JSON payload with vendor_uuids list and optional skip_access_check flag. The skip_access_check flag is a temporary parameter that mimics an existing flag in GraphQL for backwards compatibility. Example payload: {"vendor_uuids": ["uuid1", "uuid2"], "skip_access_check": false} Returns: Flask.response. """ vendor_uuids = deserialize_schema['vendor_uuids'] skip_access_check = deserialize_schema.get('skip_access_check', False) # If skip_access_check is False, perform access check if not skip_access_check: try: if not ows_permissions.get_profile_has_all_label_access(): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Access denied: profile does not have all label access', status=403, ) ) except Exception as e: return flaskify(response.create_fatal_response(e.args)) try: return flaskify(response.Response(vendor.get_vendor_service_tier(vendor_uuids))) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/vendor', methods=['PATCH']) @validate_request_data(create_vendor.CreateVendorSchema()) @tracer.wrap() def create_update_vendor(deserialize_schema): """Create a new vendor or update an existing vendor. *DEPRECATION WARNING:* This endpoint is exclusively for OA and Workstation(?) usage, which access it through GraphQL. It will eventually be replaced by a series of v2 endpoints that authorize solely via PP. The transition has already begun with: - POST /v2/vendors - PATCH /v2/vendor//external-identifier-1. Params: vendor_id (int): Required for updates. Unique identifier for a vendor. vendor_name (str): Required for creates. email (str): Required. email + vendor_name = unique identifier for new vendor create. owner (str | None): Optional. owner name. Default is orchard. label_identifier (str): Optional. label identifier. status (str | None): Optional. label status. Default is signed. company_brand (str | None): Optional. The Company's brand this vendor belongs to. service_tier_uuid (str): Required. uuid of the service tier the vendor will be in. migrated_to_abacus (bool | None): Optional. country (str | None): Optional. Country affiliated with brand, iso alpha 3 format. assigned_to (int | None): Optional. orchardadmin_user id the vendor is assigned to. assigned_reviewer (int | None): Optional. Foreign key to orchadmin_users table. source (str): Required. The source that is creating the vendor closer_id (int | None): Optional. Closer ID to assign to the vendor (creates only). Returns: Flask.response. """ g.ows.log.warn( 'PATCH /vendor is being deprecated.', resources={ 'jwt_identity_id': g.request_context.jwt_identity_id, 'identity_id': g.request_context.identity_id, 'profile_id': g.request_context.profile_id, 'profile_type': g.request_context.profile_type, }, ) access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) if not g.request_context.context_type == header_constants.CONTEXT_TYPE_PROFILE: return flaskify(error_response.create_error_forbidden()) @Neo4jSession(transaction=True, use_v2=True, database=NEO4J_DATABASE_NAME) def call_logic(): return flaskify(vendor.create_or_update_vendor(deserialize_schema)) return call_logic() @tracer.wrap() @app.route('/v2/vendors', methods=['POST']) @Neo4jSession(transaction=True, use_v2=True, database=NEO4J_DATABASE_NAME) @validate_request_data(create_vendor.V2CreateVendorSchema()) def v2_create_vendor(deserialize_schema): """Create a vendor with minimal attributes. Required: name (str): Full name of the Vendor. owner (str): Owner of this Vendor (what business unit it maps back to). company_brand (str): Company's brand name. eg: awal, columbia, theorchard, hrs. service_tier_uuid (str): Unique id for service tier. payment_currency (str): 3 char currency code. Not validated. Not persisted to the vendor table — passed through to the post-creation account event payload. Optional: is_distributor (bool): Whether this vendor is a distributor. Defaults to False. country (str): ISO alpha-3 country code. eg: USA, FRA. primary_genre (int): Primary genre id. label_summary (str): Free-text summary of the label. assigned_to (int): Relationship Manager — orchadmin_users.id responsible for client relationship. assigned_reviewer (int): Content Review assignee — orchadmin_users.id who approves product before delivery. quarterback_label_manager (int): Secondary Relationship Manager — orchadmin_users.id as backup contact. wel_email_sender (int): Welcome Email Sender — orchadmin_users.id who triggered the welcome email. product_manager (int): Product Manager — orchadmin_users.id who manages the label's product delivery. Returns: Flask.response """ request_data = request.json company_brand = request_data['company_brand'] # Use Authorization Backend to verify # identity has permissions to create an account # under the desired company_brand if not authorization_backend.is_authorized( action='create', resource_id=0, resource_type='account', resource_getter=CompanyBrandByNameResourceGetter(company_brand), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) identity_id = g.request_context.jwt_identity_id oa_user_id = identity_logic.get_oa_user_id(identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) request_data['user_id'] = oa_user_id request_data['identity_id'] = g.request_context.jwt_identity_id result_response = vendor.create_vendor(request_data) return flaskify(result_response) @tracer.wrap() @app.route('/v2/vendors/', methods=['DELETE']) def v2_delete_vendor(vendor_uuid: str): """Idempotently soft-delete a vendor by setting its status to 'deletion'. Args: vendor_uuid (str): UUID of the vendor to delete. Returns: Flask.response: 200 with {'vendor_uuid': ...}. Repeat calls return the same response shape (idempotent). """ # required PP role is account_admin if not authorization_backend.is_authorized( action='delete', resource_id=vendor_uuid, resource_type='account', resource_getter=AccountByUuidResourceGetter(vendor_uuid), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) oa_user_id = identity_logic.get_oa_user_id(g.request_context.jwt_identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) try: vendor.delete_vendor(vendor_uuid, oa_user_id) except Exception: return flaskify(response.create_fatal_response('Unable to delete vendor')) return flaskify(response.Response(status=200, message={'vendor_uuid': vendor_uuid})) @app.route('/vendor/', methods=['PATCH']) @validate_request_data(UpdateVendorSchema()) def update_vendor(vendor_id, deserialize_schema): """Update partial vendor information. This endpoint is exclusively for OA usage, which accesses it through GraphQL. It will eventually be replaced by a series of v2 endpoints that authorize solely via PP. The transition has already begun with PATCH /v2/vendor//external-identifier-1. Args: -vendor_id (int): unique identifier for a vendor. Properties: -name (str): full name of the Vendor -company_brand (str): Company's brand name. eg: awal, columbia, theorchard, hrs -company_brand_uuid (str): unique id for company brand. -service_tier_uuid (str): unique id for service tier. -label_identifier (enum): 'Frontline','Client Services','Catalog','D3','Film','TV','Test'. . . . for full list of supported updatable properties see update_vendor validation schema. Returns: Flask.response: field(s) changed and update timestamp """ g.ows.log.warn( 'PATCH /vendor/ is being deprecated.', resources={ 'jwt_identity_id': g.request_context.jwt_identity_id, 'identity_id': g.request_context.identity_id, 'profile_id': g.request_context.profile_id, 'profile_type': g.request_context.profile_type, }, ) access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) if not g.request_context.context_type == header_constants.CONTEXT_TYPE_PROFILE: return flaskify(error_response.create_error_forbidden()) @Neo4jSession(transaction=True, use_v2=True, database=NEO4J_DATABASE_NAME) def call_logic(): user_id = request.headers.get(header_constants.ORCHARD_USER_ID) if user_id: if user_id.startswith('oa:'): deserialize_schema['last_modified_by'] = user_id.lstrip('oa:') else: return flaskify( response.Response( status=401, message=(f'Unauthorized user with user_id = {user_id}'), ) ) return flaskify(vendor.update_vendor(vendor_id, deserialize_schema)) return call_logic() @app.route('/vendor//closers', methods=['PUT']) @validate_request_data(UpdateVendorClosersSchema()) def update_vendor_closers(vendor_uuid: str, deserialize_schema): """Update vendor closers for a vendor using uuid.""" closers = deserialize_schema['closers'] access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) if g.request_context.jwt_identity_id: oa_user_id = identity_logic.get_oa_user_id(g.request_context.jwt_identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=401, ) ) else: oa_user_id = request.headers.get(header_constants.ORCHARD_USER_ID) if oa_user_id: if oa_user_id.startswith('oa:'): oa_user_id = int(oa_user_id.lstrip('oa:')) else: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=f'Unauthorized user with user_id = {oa_user_id}', status=401, ) ) try: result = vendor.update_vendor_closers(vendor_uuid, closers, oa_user_id) except VendorUpdateException as e: return flaskify( response.create_error_response(code=e.code, message=e.message, status=e.status) ) return flaskify(response.Response(result)) @app.route('/vendor//first_statement_period', methods=['PATCH']) @validate_request_data(UpdateVendorFirstStatementPeriodSchema()) def update_vendor_first_statement_period(vendor_uuid: str, deserialize_schema): """Update vendor first_statement_period for a vendor using uuid.""" first_statement_period = deserialize_schema['first_statement_period'] access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) if g.request_context.jwt_identity_id: oa_user_id = identity_logic.get_oa_user_id(g.request_context.jwt_identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=401, ) ) else: oa_user_id = request.headers.get(header_constants.ORCHARD_USER_ID) if oa_user_id: if oa_user_id.startswith('oa:'): oa_user_id = int(oa_user_id.lstrip('oa:')) else: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=f'Unauthorized user with user_id = {oa_user_id}', status=401, ) ) return flaskify( vendor.update_vendor_first_statement_period(vendor_uuid, first_statement_period) ) @app.route('/v2/vendor//external-identifier-1', methods=['PATCH']) @validate_request_data(UpdateVendorExternalIdentifier1Schema()) def v2_update_vendor_external_identifier_1(vendor_uuid: str, deserialize_schema): """Update only the external_identifier_1 field for a vendor.""" if not authorization_backend.is_authorized( action='update_external_identifier_1', resource_id=vendor_uuid, resource_type='account', resource_getter=AccountByUuidResourceGetter(vendor_uuid), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) oa_user_id = identity_logic.get_oa_user_id(g.request_context.jwt_identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) try: result = vendor.update_vendor_external_identifier_1( vendor_uuid, oa_user_id, deserialize_schema['external_identifier_1'], ) except VendorUpdateException as e: return flaskify( response.create_error_response(code=e.code, message=e.message, status=e.status) ) return flaskify(response.Response(result)) @app.route('/v2/vendor//country_id', methods=['PATCH']) @validate_request_data(UpdateVendorCountryIdSchema()) def v2_update_vendor_country_id(vendor_uuid: str, deserialize_schema): """Update only the country field for a vendor.""" if not authorization_backend.is_authorized( action='update:country_id', resource_id=vendor_uuid, resource_type='account', resource_getter=AccountByUuidResourceGetter(vendor_uuid), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) oa_user_id = identity_logic.get_oa_user_id(g.request_context.jwt_identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) try: result = vendor.update_vendor_country_id( vendor_uuid, oa_user_id, deserialize_schema['country_id'], ) except VendorUpdateException as e: return flaskify( response.create_error_response(code=e.code, message=e.message, status=e.status) ) return flaskify(response.Response(result)) @app.route('/v2/vendor//service_tier', methods=['PATCH']) @jwt_check @validate_request_data(UpdateVendorServiceTierSchema()) def v2_update_vendor_service_tier(vendor_uuid: str, deserialize_schema): """Update the service tier for a vendor.""" if not authorization_backend.is_authorized( action='update:service_tier', resource_id=vendor_uuid, resource_type='account', resource_getter=AccountByUuidResourceGetter(vendor_uuid), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) oa_user_id = identity_logic.get_oa_user_id(g.request_context.jwt_identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) try: result = vendor.update_vendor_service_tier( vendor_uuid, oa_user_id, deserialize_schema['service_tier_uuid'], ) except VendorUpdateException as e: return flaskify( response.create_error_response(code=e.code, message=e.message, status=e.status) ) return flaskify(response.Response(result)) @app.route('/v2/vendor//internal-staff', methods=['PATCH']) @jwt_check @validate_request_data(UpdateVendorInternalStaffSchema()) def v2_update_vendor_internal_staff(vendor_uuid: str, deserialize_schema): """Update internal staff assignments for a vendor.""" if not authorization_backend.is_authorized( action='update:internal_staff', resource_id=vendor_uuid, resource_type='account', resource_getter=AccountByUuidResourceGetter(vendor_uuid), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) oa_user_id = identity_logic.get_oa_user_id(g.request_context.jwt_identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) # Forward only keys the client actually sent so the logic layer can # distinguish "field omitted" (no change) from "field set to null" (clear). # e.g. {'assigned_to': 123, 'quarterback_label_manager': None} staff_kwargs = {f: deserialize_schema[f] for f in STAFF_FIELDS if f in deserialize_schema} try: result = vendor.update_vendor_internal_staff( vendor_uuid, oa_user_id, **staff_kwargs, ) except VendorUpdateException as e: return flaskify( response.create_error_response(code=e.code, message=e.message, status=e.status) ) return flaskify(response.Response(result)) @app.route('/v2/vendor/', methods=['PATCH']) @jwt_check @validate_request_data(UpdateVendorInfoSchema()) def v2_update_vendor_info(vendor_uuid: str, deserialize_schema): """Update vendor info (name, owner, support_contact_email, etc).""" if not authorization_backend.is_authorized( action='update:info', resource_id=vendor_uuid, resource_type='account', resource_getter=AccountByUuidResourceGetter(vendor_uuid), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) oa_user_id = identity_logic.get_oa_user_id(g.request_context.jwt_identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) metadata_kwargs = { f: deserialize_schema[f] for f in UpdateVendorInfoSchema.METADATA_FIELDS if f in deserialize_schema } try: result = vendor.update_vendor_info( vendor_uuid, oa_user_id, **metadata_kwargs, ) except VendorUpdateException as e: return flaskify( response.create_error_response(code=e.code, message=e.message, status=e.status) ) return flaskify(response.Response(result)) @app.route('/vendor/brand/', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_company_brand(vendor_id): """Return company brand associated with given label. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ use_managed_tx = ( pythonfeatures.get_single_feature(FEATURE_USE_NEO4J_MANAGED_TX, g.request_context) ).message == split_constants.FEATURE_ENABLED access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) if use_managed_tx: return flaskify(vendor.get_vendor_company_brand_tx(vendor_id)) return flaskify(vendor.get_vendor_company_brand(vendor_id)) @app.route('/vendor//suppliers', methods=['GET']) def get_phys_suppliers(vendor_id): """Return vendor suppliers default selection. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(supplier.get_suppliers(vendor_id)) @app.route('/vendor//suppliers', methods=['PUT']) def set_phys_suppliers(vendor_id): """Set suppliers default selection. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) data = request.get_json(silent=True, force=True) or {} store_ids = data.get('store_ids') if store_ids is None: store_ids = [] return flaskify(supplier.set_suppliers(vendor_id, store_ids)) @app.route('/vendor//features', methods=['GET']) def get_enabled_features_for_vendor(vendor_id): """Get a list of enabled feature controls for a vendor. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response: containing the list of enabled feature controls. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) validation = flask_request.verify_grass_access(request, vendor=vendor_id) if not validation: return flaskify(response=validation) return flaskify(feature.get_enabled_features_for_vendor(vendor_id)) @app.route('/vendor/features/', methods=['GET']) def get_bulk_vendors_with_enabled_feature(feature_id): """Get a list of vendors with a given feature enabled. Args: feature_id (int): feature ID to filter by Returns: Flask.response: list of vendors with the feature enabled. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) vendors = ows_permissions.get_vendor_access() vendor_star = [v for v in vendors if v['vendor_id'] == '*'] if len(vendor_star) != 1: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) page = pagination.get_pagination(request) return flaskify( response.Response( message=feature_model.get_vendors_with_feature( feature_id, page_offset=page.offset, page_limit=page.limit, ) ) ) @app.route('/vendor//restricted_features/add', methods=['POST']) @validate_request_data(BulkAddVendorRestrictedFeaturesSchema()) def bulk_add_restricted_features_for_vendor(vendor_id, deserialize_schema): """Bulk add restricted features for a vendor. POST Body: feature_ids (list[int]): list of feature IDs. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify( feature.bulk_add_restricted_features_for_vendor( vendor_id, deserialize_schema['feature_ids'] ) ) @app.route('/vendor//restricted_features/remove', methods=['POST']) @validate_request_data(BulkRemoveVendorRestrictedFeaturesSchema()) def bulk_remove_restricted_features_for_vendor(vendor_id, deserialize_schema): """Bulk remove restricted features for a vendor. POST Body: feature_ids (list[int]): list of feature IDs. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify( feature.bulk_remove_restricted_features_for_vendor( vendor_id, deserialize_schema['feature_ids'] ) ) @app.route('/vendor//assigned-to', methods=['GET']) def get_vendor_assigned_to(vendor_id): """Get a single vendor's assigned_to property.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) result = vendor.get_vendor_assigned_to(vendor_id) return flaskify(result) @app.route('/vendor//secondary-internal-contact', methods=['GET']) def get_vendor_secondary_internal_contact(vendor_id): """Get a single vendor's secondary internal contact.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) result = vendor.get_vendor_secondary_internal_contact(vendor_id) return flaskify(result) @app.route('/vendor//document', methods=['GET']) @validation.reject_grass_headers def get_vendor_document(vendor_id): """Get single vendor document for cloudsearch corpus. Should have no grass headers since this is intended for internal use Args: vendor_id (int): vendor id Returns: Response: Flask response """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) import json with_tenant_uuids = request.args.get('with_tenant_uuids', False, type=json.loads) return flaskify(vendor.get_vendor_document(vendor_id, with_tenant_uuids)) @app.route('/vendor//assigned-reviewer', methods=['GET']) def get_vendor_assigned_reviewer(vendor_id): """Get a single vendor's assigned_reviewer property.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) result = vendor.get_vendor_assigned_reviewer(vendor_id) return flaskify(result) @app.route('/vendor//service-details', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_vendor_service_details(vendor_id): """Get vendor service details.""" access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) validation = flask_request.verify_grass_access(request, required=False, vendor=vendor_id) if not validation: return flaskify(validation) vendor_service_details = vendor.get_vendor_service_details(vendor_id) return flaskify(vendor_service_details) @app.route('/vendor//distributor', methods=['PATCH']) def set_vendor_distributor(vendor_id): """Set is_distributor field from N to Y. Args: vendor_id (int): unique identifier for a vendor. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) return flaskify(vendor.update_is_distributor_in_vendor(vendor_id)) @app.route('/vendors/by-external-identifier-1/', methods=['GET']) def get_vendors_by_external_identifier(external_identifier_1): """Get a list of vendors by external_identifier_1. Note: This endpoint is only for internal use in process of creation SME vendors. Args: external_identifier_1 (str): external_identifier_1. Returns: Flask.response. """ access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=401, ) ) owner_name = request.args.get('owner') result = vendor.get_vendors_by_external_identifier(external_identifier_1, owner_name) return flaskify(result) @app.route('/vendor//master-contact', methods=['GET']) @Neo4jSession(use_v2=True, database=NEO4J_DATABASE_NAME) def get_master_contact(vendor_id): """Get master contact for vendor.""" if not authorization_backend.is_authorized( action='view_account_info', resource_id=vendor_id, resource_type='account', resource_getter=AccountByIdResourceGetter(vendor_id), ): return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Unauthorized', status=403, ) ) return flaskify(vendor.get_master_contact(vendor_id)) @app.route('/vendor//relationship_notes', methods=['PATCH']) @validate_request_data(UpdateVendorNotesSchema()) def update_vendor_notes(vendor_uuid: str, deserialize_schema): """Handler to update relationship notes for a vendor. Expects JSON payload with relationship notes string. Example payload: {"relationship_notes": "These are some relationship notes."} Args: vendor_uuid (str): unique identifier for a vendor. deserialize_schema (dict): validated request data containing relationship_notes. Returns: Flask.response. """ relationship_notes = deserialize_schema['relationship_notes'] access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) if g.request_context.jwt_identity_id: oa_user_id = identity_logic.get_oa_user_id(g.request_context.jwt_identity_id) if not oa_user_id: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=401, ) ) else: oa_user_id = request.headers.get(header_constants.ORCHARD_USER_ID) if oa_user_id: if oa_user_id.startswith('oa:'): oa_user_id = int(oa_user_id.lstrip('oa:')) else: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message=f'Unauthorized user with user_id = {oa_user_id}', status=401, ) ) try: return flaskify(vendor.update_vendor_notes(vendor_uuid, relationship_notes)) except Exception as e: return flaskify(response.create_fatal_response(e.args)) @app.route('/vendors/relationship_notes/dataloader', methods=['POST']) @validate_request_data(VendorRelationshipNotesRequestSchema()) def get_vendors_relationship_notes_handler(deserialize_schema): """Handler to fetch relationship notes for a vendor. Returns the stored relationship notes HTML string. Args: vendor_uuid (str): Unique identifier for a vendor. Returns: Flask.response. """ vendor_uuids = deserialize_schema['vendor_uuids'] access_rule_decision = flask_request.verify_rules_access_standalone(request) if not access_rule_decision: return flaskify( response.create_error_response( code=error.ERROR_CODE_AUTHORIZATION, message='Forbidden', status=403, ) ) try: return flaskify(response.Response(vendor.get_vendors_relationship_notes(vendor_uuids))) except Exception as e: return flaskify(response.create_fatal_response(str(e)))