"""set-carveouts.""" import uuid from datetime import datetime from typing import Dict, List, Optional from ddex_ingester_common.constants.country_codes import (ALL_COUNTRY_CODES, WORLDWIDE) from ddex_ingester_common.constants.ddex_providers import ( RELEASE_CORRECTION_PROVIDERS, SME) from ddex_ingester_common.constants.release_type import VIDEO_RELEASE_TYPES from ddex_ingester_common.constants.status import (IN_CONTENT, TRANSFER_TO_CONTENT) from ddex_ingester_common.constants.swb_deal_types import \ AWAL_NOT_OUR_DISTRIBUTION from ddex_ingester_common.helpers.lambda_warning import LambdaWarning from ddex_ingester_common.helpers.s3_ddex import load_ddex_json from ddex_ingester_common.lambda_exceptions import CarveoutException from ddex_ingester_common.logging import utils as logging_utils from ddex_ingester_common.models.s3.body import Body as S3Context from ddex_ingester_common.models.state_machine.body import \ Body as StateMachineContext from ddex_ingester_common.release_correction.release_correction import \ is_release_correction from ddex_ingester_common.release_correction.release_correction_diffs import \ ReleaseCorrectionDiffDetail from ddex_ingester_common.release_correction.release_corrections_s3 import ( create_blank_rc_json, write_rc_json) from ddex_ingester_common.schemas.s3_schema import S3Schema from ddex_ingester_common.schemas.state_machine_schema import \ StateMachineSchema from lambdacommon.graphql import graphql import config from constants import queries from constants.set_carveouts import RC_JSON_NAME logger = logging_utils.get_logger(config.app_logger) def handler(event, context): """Lambda entry point.""" context = StateMachineSchema().load(event) s3_data = S3Schema().load(load_ddex_json(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, s3_data.message_id, s3_data.message_thread_id, s3_data.execution_name ) if s3_data.product.not_for_distribution == AWAL_NOT_OUR_DISTRIBUTION: # Skip this Lambda as there are no Deal Terms return StateMachineSchema().dump(context) product_deal_terms = get_product_deal_terms(s3_data) if not product_deal_terms: raise CarveoutException('Product Deal Terms not found.') logger.info(f'Got product deal terms: {product_deal_terms}') carveout_data = format_carveout_data( product_deal_terms, context.product.product_id, context.product.upc ) if not carveout_data: raise CarveoutException('Product Deal includes a Takedown.') try: graphql_conn = graphql.GraphQLConnector( config.GRAPHQL_GATEWAY_URL, config.APPLICATION_NAME) graphql_conn.set_headers( { 'Orchard-User-Id': config.OA_USER, 'Correlation-Id': correlation_id, } ) # get complement territories country_codes = map_country_codes(carveout_data['countryCodes']) carveout_data['countryCodes'] = country_codes if context.product.release_type in VIDEO_RELEASE_TYPES and \ context.product.status in [IN_CONTENT, TRANSFER_TO_CONTENT] and \ context.ddex_provider in RELEASE_CORRECTION_PROVIDERS: logger.info('Skipping video carveout update') check_for_unsupported_updates( context, country_codes, True) elif is_release_correction(context): logger.info('Skipping carveout update') check_for_unsupported_updates( context, country_codes) else: # call ows-carveouts logger.info(f'Setting carveout data: {carveout_data}') graphql_conn.execute( queries.set_carveouts, {'data': carveout_data}) set_store_carveouts( graphql_conn, s3_data, context) except graphql.GraphQLError as err: raise CarveoutException('Graphql error') from err return StateMachineSchema().dump(context) def format_carveout_data( deal_terms: List, product_id: int, upc: str) -> Optional[Dict]: """Extract territories from product deal terms.""" territories = [] for deal_term in deal_terms: if deal_term.takedown: return None # If we encounter a deal EndDate in the past, skip this term. end_date = deal_term.end_date_time or deal_term.end_date if end_date: formatted_date = datetime.strptime(end_date[:10], '%Y-%M-%d') if formatted_date < datetime.now(): end_date_territories = deal_term.territories logger.info(f'End date found for deal term: {end_date} ' f'Skipping these territories: {end_date_territories}') # noqa continue if deal_term.territories: if WORLDWIDE in deal_term.territories: territories = ALL_COUNTRY_CODES else: territories.extend(deal_term.territories) elif deal_term.excluded_territories: territories.extend( list(set( ALL_COUNTRY_CODES) - set(deal_term.excluded_territories))) return { 'productId': product_id, 'upc': upc, 'countryCodes': territories, } def map_country_codes(country_codes: List) -> List: """Map SME country codes to Orchard.""" mapped_country_codes = set() no_mapping_codes = set() logger.info(f'Attempting to map country codes: {country_codes}') for country_code in country_codes: if config.SME_ORCH_COUNTRY_MAPPING.get(country_code): logger.info( f'Mapping {country_code} to' f' {config.SME_ORCH_COUNTRY_MAPPING.get(country_code)}' ) mapped_country_codes.add( config.SME_ORCH_COUNTRY_MAPPING.get(country_code)) else: no_mapping_codes.add(country_code) mapped_country_codes.add(country_code) logger.info(f'Added country codes without mapping: {no_mapping_codes}') return sorted(list(set(ALL_COUNTRY_CODES) - mapped_country_codes)) def get_product_deal_terms(s3_data: S3Context) -> Optional[List]: """Get deal term for R0 if not then R1 else None.""" for deal in s3_data.deals: if 'R0' in deal.release_references: return deal.deal_terms if 'R1' in deal.release_references: return deal.deal_terms return None def check_for_unsupported_updates( context: StateMachineContext, new_country_codes: List, is_video: bool = False): """Check if unsupported fields are updated.""" # Compare new and existing country codes error_message = '' original_codes = None if context.product.original_values: original_codes =\ context.product.original_values.carveout_country_codes sorted_new_codes = sorted(new_country_codes or []) or 'No value found' sorted_original_codes = sorted(original_codes or []) or 'No value found' if sorted_new_codes != sorted_original_codes: field_name = 'carveouts' # For videos we write this to RC Json. For audio, context. if is_video: update_diff_details = [] update_diff_details.append(ReleaseCorrectionDiffDetail( field_name=field_name, isrc=None, old=sorted_original_codes, new=sorted_new_codes)) if update_diff_details: logger.info(f'Carveout differences found. Current: {sorted_original_codes} ' # noqa f'New values: {sorted_new_codes}') event_details = {'bucket': context.bucket, 'key': context.key} s3_release_corrections = {'changes': update_diff_details} create_blank_rc_json(event_details, RC_JSON_NAME) write_rc_json(event_details, s3_release_corrections, RC_JSON_NAME) # noqa else: if type(context.warnings) is not list: context.warnings = [] error_message += ( f'Product Carveouts update attempt. DDEX values ' f'{sorted_new_codes} ' f"does not match The Orchard's Carveouts " f'{sorted_original_codes}\n') # Remove last \n error_message = error_message[:-1] context.warnings.append( LambdaWarning( field_name, 'ReleaseCorrectionUpdateException', error_message )._asdict() ) def set_store_carveouts( graphql_conn: graphql.GraphQLConnector, s3_data: S3Context, context: StateMachineContext): """Set product store carveouts.""" product_id = context.product.product_id distribution_type_ids = config.STORE_CARVEOUT_DISTRIBUTION_TYPES if context.ddex_provider != SME \ and context.product.release_type in VIDEO_RELEASE_TYPES: distribution_type_ids = config.VIDEO_STORE_CARVEOUT_DISTRO_TYPES store_carveouts = [] for store in s3_data.product.stores or []: if store.carved_out and store.store_id: store_carveouts.append({ 'storeId': int(store.store_id), 'distributionTypeIds': distribution_type_ids }) if store_carveouts: payload = { 'productId': product_id, 'storeCarveouts': store_carveouts, } logger.info(f'Setting store carveouts: {payload}') graphql_conn.execute(queries.set_store_carveouts, {'data': payload})