"""lookup-switchboard-deal.""" import json import uuid import boto3 from ddex_ingester_common.constants.swb_deal_types import (FOR_DISTRIBUTION, SWBDUMMY) from ddex_ingester_common.helpers.s3_ddex import load_ddex_json from ddex_ingester_common.helpers.switchboard import generate_switchboard_jwt from ddex_ingester_common.lambda_exceptions import ( LambdaException, LookupSwitchboardDealException, LookupSwitchboardInactiveDealException) from ddex_ingester_common.logging import utils as logging_utils from ddex_ingester_common.models.s3.body import Body as S3Body from ddex_ingester_common.models.state_machine.body import \ Body as StateMachineBody from ddex_ingester_common.schemas.s3_schema import S3Schema from ddex_ingester_common.schemas.state_machine_schema import \ StateMachineSchema from marshmallow.utils import get_value import config from constants.deal_responsibilities import (IMPORTANT_COUNTRIES, INCLUDED, NOT_APPLICABLE, WORLDWIDE) from constants.special_instructions import (NUM_OF_COUNTRIES_TO_FORMAT, RESPONSIBILITY_TEXT, SPECIAL_INSTRUCTION_TEXT) from constants.swb_queries import GET_PRODUCT_BY_UPC from helpers.upc_remap import get_remapped_upc_rows from helpers.vendor_mapping import ( get_vendor_and_subaccount_for_major_label_rows, get_vendor_and_subaccount_for_rep_owner_code_rows) logger = logging_utils.get_logger(config.app_logger) def is_ccm_som_livre_grps_ddex_ingestion_enabled(): """Lookup if ccm_som_livre_grps_ddex_ingestion split is enabled.""" split = config.split_client is_enabled = split and split.get_treatment( config.APPLICATION_NAME, 'ccm_som_livre_grps_ddex_ingestion', {'service': config.APPLICATION_NAME} ) == 'on' return is_enabled def handler(event, context): """Lambda entry point.""" if config.SKIP_LAMBDA and config.ENVIRONMENT != config.PROD_ENVIRONMENT: return event context = StateMachineSchema().load(event) correlation_id = context.correlation_id or str(uuid.uuid4()) context.correlation_id = correlation_id logging_utils.update_logger_correlation_id(logger, correlation_id) logging_utils.update_logger_with_message_ids( logger, context.message_id, context.message_thread_id, context.execution_name ) config.graphql_switchboard.set_headers( {'x-token': generate_switchboard_jwt( config.SWITCHBOARD_ROLE_ARN, config.SWITCHBOARD_SECRET_ARN, config.APPLICATION_NAME )}) # Purged releases do not produce parsed_ddex.json files purged_release = context.is_purged_release s3_context = S3Schema().load(load_ddex_json(event)) if not purged_release else None # noqa: E501 if s3_context: parent_rep_owner_code = s3_context.product.parent_repertoire_owner_code rep_owner_code = s3_context.product.repertoire_owner_code maint_owner_code = s3_context.product.maintenance_owner_code # For multiple rep owner codes, we need to use maintenance owner if (parent_rep_owner_code == config.MULTIPLE_REP_OWNERS_CODE and rep_owner_code == config.MULTIPLE_REP_OWNERS_CODE and maint_owner_code): s3_context.product.parent_repertoire_owner_code = maint_owner_code s3_context.product.repertoire_owner_code = maint_owner_code # If this is a remap, we want to use the original UPC # This prevents Switchboard checking for a UPC that doesn't exist yet. original_upc = get_original_remap_upc(context, s3_context) get_switchboard_data(context, original_upc, s3_context) if not purged_release: # Update s3_context with target vendor and subaccount s3_context.product.vendor_id = context.product.vendor_id s3_context.product.subaccount_id = context.product.subaccount_id save_s3_context(context, s3_context) return StateMachineSchema().dump(context) def get_switchboard_data( context: StateMachineBody, upc: int, s3_context: S3Body): """Set switchboard company data onto Context.""" logger.info('Executing Switchboard graphql query') if upc: logger.info(f'Detected UPC remap, using original upc: {upc}') else: # Use the regular upc upc = context.product.upc switchboard_product = config.graphql_switchboard.execute( GET_PRODUCT_BY_UPC, { 'upc': upc, 'system': 'SONY' } ) logger.info(f'Switchboard GraphQL response: {switchboard_product}') prod_by_upc = get_value(switchboard_product, 'data.getProductByUpc', None) deal_id = get_value( prod_by_upc, 'switchboardExtensions.governingDeal.dealId', None) if not deal_id: context.has_switchboard_deal = False if not is_ccm_som_livre_grps_ddex_ingestion_enabled(): # There's no deal associated with the given Product UPC raise LookupSwitchboardDealException( f'No Switchboard deal found for UPC: {upc}') # Only look up vendor if there is no deal and no takedown if not context.is_purged_release: # Check for major label mapping based on major label or rep owner vendor_id, subaccount_id = \ get_vendor_and_subaccount_for_major_label(context, s3_context) context.product.vendor_id = vendor_id if subaccount_id: context.product.subaccount_id = subaccount_id context.product.not_for_distribution = 'N' return context.has_switchboard_deal = True deal_status = get_value( prod_by_upc, 'switchboardExtensions.governingDeal.status', None) if not deal_status or deal_status != 'ACTIVE': raise LookupSwitchboardInactiveDealException( f'Switchboard deal is not Active. Deal status: {deal_status}') maintenance_owner = get_value( prod_by_upc, 'maintenanceOwner.name', None) switchboard_vendor_id = get_value( prod_by_upc, 'labelAccount.companyCode', None) switchboard_subaccount_id = get_value( prod_by_upc, 'subAccount.companyCode', None) switchboard_special_instructions = format_special_instructions(prod_by_upc) deal_responsibilities = get_value( prod_by_upc, 'switchboardExtensions.governingDeal.dealResponsibilities', []) deal_type = define_swb_deal_type(deal_responsibilities) context.product.not_for_distribution = deal_type context.maintenance_owner = maintenance_owner context.product.vendor_id = switchboard_vendor_id context.product.subaccount_id = switchboard_subaccount_id context.product.special_instructions = switchboard_special_instructions # noqa context.deal_coordinators = get_value( prod_by_upc, 'switchboardExtensions.governingDeal.dealCoordinators', None) def define_swb_deal_type(responsibilities): """Define swb deal distribution value.""" for responsibility in responsibilities: if get_value(responsibility, 'dealResponsibilityType.name', None) ==\ 'Digital Distribution': for assignment in responsibility.get('assignments', []): if assignment.get('system') == 'ORCHARD': return FOR_DISTRIBUTION return SWBDUMMY def format_special_instructions(product_data): """Format special instructions.""" # TODO: We need a data structure to abstract the retrival of data try: deal_responsibilities = product_data['switchboardExtensions'][ 'governingDeal']['dealResponsibilities'] except (KeyError, TypeError) as e: logger.info(f'Error on format_special_instructions: {e}') return '' formatted_special_instructions = [] for responsibility in deal_responsibilities: applies_to = responsibility.get('appliesTo') if applies_to == NOT_APPLICABLE: formatted_special_instructions.append( format_not_applicable_responsibility(responsibility) ) elif applies_to == WORLDWIDE: formatted_special_instructions.append( format_worldwide_responsibility(responsibility) ) else: for assignment in responsibility['assignments']: formatted_special_instructions.append( format_assigned_responsibility(responsibility, assignment, ) ) formatted_special_instructions.sort() intro_text = SPECIAL_INSTRUCTION_TEXT return '\n'.join( [intro_text] + formatted_special_instructions) def format_not_applicable_responsibility(responsibility): """Format not applicable responsibility.""" return RESPONSIBILITY_TEXT.format( responsibility['dealResponsibilityType']['name'], NOT_APPLICABLE, '').strip() def format_worldwide_responsibility(responsibility): """Format worldwide responsibility.""" owner = [assignment['system'] for assignment in responsibility['assignments']][0] return RESPONSIBILITY_TEXT.format( responsibility['dealResponsibilityType']['name'], owner, WORLDWIDE).strip() def format_assigned_responsibility(responsibility, assignment): """Format assigned responsibility.""" important_countries = { key: value for value, key in enumerate(IMPORTANT_COUNTRIES) } territories = assignment['territories'] # Sort important countries to the start of list territories.sort(key=lambda key: important_countries.get( key, len(important_countries) + 1)) formatted_territories = '{} {} {}'.format( 'includes' if assignment['scope'] == INCLUDED else 'excludes', ', '.join(territories[:NUM_OF_COUNTRIES_TO_FORMAT]) if territories else 'no territories', 'and some other countries' if len(territories) > NUM_OF_COUNTRIES_TO_FORMAT else '' ) return RESPONSIBILITY_TEXT.format( responsibility['dealResponsibilityType']['name'], assignment['system'] + ' -', formatted_territories).strip() def save_s3_context(context: StateMachineBody, s3_context: S3Body): """Save s3 context.""" s3_client = boto3.client('s3') json_file_path = f'{context.key}parsed_ddex.json' s3_client.put_object( Bucket=context.bucket, Key=json_file_path, Body=json.dumps(S3Schema().dump(s3_context)).encode(encoding='UTF-8') ) def get_original_remap_upc(context: StateMachineBody, s3_context: S3Body): """Return the original UPC if it's a remapped product.""" logger.info('Checking UPC remap for SME.') if not context.product or not s3_context or not s3_context.product: return rows = get_remapped_upc_rows(logger, context) # If there is no entry in the DB for this UPC, do nothing if not rows: return # There should only be one row for each (UPC, DDEX Provider) set if len(rows) > 1: raise LambdaException(f'Multiple UPC mappings found: {rows}') original_upc = rows[0].get('original_upc') if not original_upc: logger.info('UPC remap not found. Erroring out.') raise LambdaException( 'Row exists for UPC but no original_upc found.') return original_upc def get_vendor_and_subaccount_for_major_label( context: StateMachineBody, s3_context: S3Body): """Return the Orchard vendor/subaccount for a GRPS major label.""" logger.info('Getting the vendor/subaccount for a GRPS Major Label.') if not context.product or not s3_context or not s3_context.product: return upc = context.product.upc major_label = s3_context.product.major_label # There's no major label in the context if not major_label: # Check for vendor mapping based on GRPS rep owner code rows = get_vendor_and_subaccount_for_rep_owner_code_rows( logger, s3_context) else: # If there is a major label in the context, check for a mapping rows = get_vendor_and_subaccount_for_major_label_rows( logger, s3_context) if not rows: # If there's no major label mapping check for rep owner code rows = get_vendor_and_subaccount_for_rep_owner_code_rows( logger, s3_context) # If there is no entry in the DB for this UPC, raise exception if not rows: raise LookupSwitchboardDealException( f'Unable to lookup vendor/subaccount for UPC: {upc}') # There should only be one row for each major label if len(rows) > 1: raise LambdaException(f'Multiple major label mappings found: {rows}') vendor_id = rows[0].get('vendor_id') subaccount_id = rows[0].get('subaccount_id', None) if not vendor_id: logger.info('Vendor ID not found for major label. Erroring out.') raise LambdaException( 'Row exists for major label but no vendor_id found.') return vendor_id, subaccount_id