"""Release correction.""" import collections import json from switchboard_consumer.constants import product_status from switchboard_consumer.constants.exceptions import ( ProductSubmissionError, ReleaseCorrectionError) from switchboard_consumer.constants.participant_roles import ( PERFORMER, PRIMARY_ARTIST ) from switchboard_consumer.constants.product_status import ( SUBMITTED ) from switchboard_consumer.constants.release_correction import ( FAILED_TO_RESUBMIT_PRODUCT, FAILED_TO_UPDATE_PRODUCT_IN_RC, PRODUCT_ARTIST_ROLE_KEYS, RELEASE_CORRECTION_PRODUCT_ARTIST_FIELDS, RELEASE_CORRECTION_PRODUCT_FIELDS, RELEASE_CORRECTION_PRODUCT_SIMPLE_FIELDS, RELEASE_CORRECTION_TRACK_ARTIST_FIELDS, RELEASE_CORRECTION_TRACK_FIELDS, RELEASE_CORRECTION_TRACK_FIELDS_IGNORE_NULL, RELEASE_CORRECTION_TRACK_SIMPLE_FIELDS, RELEASE_CORRECTION_TRACK_WRITER_FIELDS, REQUIRED_ARTIST_ENTITY_FIELDS, REQUIRED_WRITER_ENTITY_FIELDS, SUPPORTED_PRODUCT_LEVEL_ARTIST_ROLES, TRACK_ARTIST_ROLE_KEY_MAPPING) from switchboard_consumer.formatters.errors import format_generic_error from switchboard_consumer.formatters.product import ( format_result_id, format_update_product_input) from switchboard_consumer.logic.email import send_unsubmit_email from switchboard_consumer.logic.release_correction.field_formatters import ( FIELD_FORMATTERS) from switchboard_consumer.utils.common import filter_keys_from_list_of_dict from switchboard_consumer.utils.graphql import is_error from switchboard_consumer.utils.message import create_processing_result def needs_release_correcting(display_status, correction_id=None): """Determine if a status needs to go through release correction""" if display_status == product_status.COMPLETED: return True elif display_status == product_status.ERROR_CORRECTION: return True elif (display_status == product_status.SUBMITTED and # noqa correction_id): return True else: return False def diff_products_for_release_correction( orchard_product, sony_product, rc_items): orchard_product_id = orchard_product['productId'] formatted_sony_product = format_update_product_input( orchard_product_id, sony_product, []) keys_to_update = {} empty_values = ('', None) for key in RELEASE_CORRECTION_PRODUCT_SIMPLE_FIELDS.keys(): swb_value = formatted_sony_product[key] orchard_value = orchard_product[key] formatter = FIELD_FORMATTERS.get(key) if swb_value != orchard_value: if swb_value in empty_values and orchard_value in empty_values: # skipping null <> '' condition continue if formatter: keys_to_update[key] = formatter( swb_value=swb_value, orchard_value=orchard_value, orchard_product=orchard_product ) else: keys_to_update[key] = json.dumps(swb_value) elif rc_items: # Check if the existing RC has the key we are dealing with table_field_pair = RELEASE_CORRECTION_PRODUCT_SIMPLE_FIELDS.get(key) # noqa item = get_newest_correction_item(rc_items, table_field_pair) if not item: continue key_value = item.get('keyValue') swb_value = formatter( swb_value=swb_value, orchard_value=orchard_value, orchard_product=orchard_product ) if formatter else json.dumps(swb_value) # If we have an item and it is different than the swb_value # we need to make sure we create a correction to change it # to match the swb_value if item and not key_value == swb_value: keys_to_update[key] = swb_value return keys_to_update def get_newest_correction_item(rc_items, table_field_pair): matched = [] for item in rc_items: table = item.get('tableName') field = item.get('fieldName') if table_field_pair == (table, field): matched.append(item) return sorted(matched, key=lambda k: k.get('releaseCorrectionDetailId'), reverse=True)[0] if matched else None def check_for_added_or_removed_tracks(orchard_tracks, sony_tracks, logger): orchard_isrcs = filter_keys_from_list_of_dict( orchard_tracks, ['isrc']) sony_isrcs = filter_keys_from_list_of_dict( sony_tracks, ['isrc'] ) if diff_list_of_dicts(orchard_isrcs, sony_isrcs): # A track has been added or removed # We can't support removing or adding tracks in RC logger.error('Tracks have been added or removed from the Product', custom_fields={ 'orchard_isrcs': orchard_isrcs, 'sony_isrcs': sony_isrcs}) raise ReleaseCorrectionError( ('Track ISRCs attached to Product mismatch between Sony and' ' Orchard - After Product Submission: Orchard does not allow' ' Track ISRCs to be modified or Tracks to be added or removed.')) def diff_tracks_for_release_correction(orchard_tracks, sony_tracks, logger): updates = collections.defaultdict(list) empty_values = ('', None) check_for_added_or_removed_tracks(orchard_tracks, sony_tracks, logger) for sony_track in sony_tracks: # Check if Sony track exists in Orchard data orchard_track = next( (track for track in orchard_tracks if track['isrc'] == sony_track['isrc']), None) for key in RELEASE_CORRECTION_TRACK_SIMPLE_FIELDS.keys(): swb_value = sony_track.get(key) orchard_tuid = orchard_track['tuid'] orchard_value = orchard_track[key] formatter = FIELD_FORMATTERS.get(key) if swb_value != orchard_value: if key in RELEASE_CORRECTION_TRACK_FIELDS_IGNORE_NULL and \ swb_value in empty_values: continue if swb_value in empty_values and orchard_value in empty_values: # skipping null <> '' condition continue if formatter: updates[orchard_tuid].append( (key, formatter( swb_value=swb_value, orchard_value=orchard_value, orchard_track=orchard_track)) ) else: updates[orchard_tuid].append(( key, json.dumps(swb_value) )) return updates def diff_list_of_dicts(list_a, list_b): """Return list of differences between two dict lists.""" list_a_differences = [ item for item in list_a if item not in list_b ] list_b_differences = [ item for item in list_b if item not in list_a ] return [*list_a_differences, *list_b_differences] def diff_product_artists(orchard_artists, sony_artists): """Diff product artists.""" correction_fields = {} orchard_artists = filter_keys_from_list_of_dict( orchard_artists, REQUIRED_ARTIST_ENTITY_FIELDS) sony_artists = filter_keys_from_list_of_dict( sony_artists, REQUIRED_ARTIST_ENTITY_FIELDS) # Filter out artistTypes from Sony that don't exist at Product level in the Orchard # noqa sony_artists = list(filter( lambda d: d['artistType'] in SUPPORTED_PRODUCT_LEVEL_ARTIST_ROLES, sony_artists )) if diff_list_of_dicts(orchard_artists, sony_artists): # Construct release correction field for product artist correction_fields = { key: [] for key in RELEASE_CORRECTION_PRODUCT_ARTIST_FIELDS.keys() } for sony_artist in sony_artists: # There is no release correction field for PRIMARY_ARTIST if sony_artist['artistType'] == PRIMARY_ARTIST: sony_artist['artistType'] = PERFORMER if sony_artist['artistType'] in PRODUCT_ARTIST_ROLE_KEYS: correction_fields.setdefault(sony_artist['artistType'], []).append( { 'artist_name': sony_artist['artistName'], 'role': sony_artist['artistType'] }) for key, value in correction_fields.items(): correction_fields[key] = json.dumps(value) return correction_fields def diff_track_artists(orchard_tracks, sony_tracks): """Diff track artists.""" updates = collections.defaultdict(list) for sony_track in sony_tracks: orchard_track = next( (track for track in orchard_tracks if track['isrc'] == sony_track['isrc']), None) orchard_tuid = orchard_track['tuid'] orchard_artists = filter_keys_from_list_of_dict( orchard_track['primaryArtists'], REQUIRED_ARTIST_ENTITY_FIELDS) sony_artists = filter_keys_from_list_of_dict( sony_track['artists'], REQUIRED_ARTIST_ENTITY_FIELDS) if diff_list_of_dicts(orchard_artists, sony_artists): # Construct release correction field for track artists for key in RELEASE_CORRECTION_TRACK_ARTIST_FIELDS.keys(): update = [] for sony_artist in sony_artists: if TRACK_ARTIST_ROLE_KEY_MAPPING.get( sony_artist['artistType'], sony_artist['artistType']) == key: update.append({ 'name': sony_artist['artistName'], 'type': sony_artist['artistType'] } ) updates[orchard_tuid].append((key, json.dumps(update))) return updates def diff_track_writers(orchard_tracks, sony_tracks): """Diff track writers.""" updates = collections.defaultdict(list) for sony_track in sony_tracks: orchard_track = next( (track for track in orchard_tracks if track['isrc'] == sony_track['isrc']), None) orchard_tuid = orchard_track['tuid'] orchard_writers = filter_keys_from_list_of_dict( orchard_track['writers'], REQUIRED_WRITER_ENTITY_FIELDS) sony_writers = filter_keys_from_list_of_dict( sony_track['writers'], REQUIRED_WRITER_ENTITY_FIELDS) if diff_list_of_dicts(orchard_writers, sony_writers): # Construct release correction field for track writers for key in RELEASE_CORRECTION_TRACK_WRITER_FIELDS.keys(): update = [] for sony_writer in sony_writers: if sony_writer['writerType'] == 'writer': update.append(sony_writer['writerName']) updates[orchard_tuid].append((key, json.dumps(update))) return updates def create_release_correction(product_id, orchard_client, correlation_id, logger): """Create a release correction.""" release_correction = orchard_client.create_release_correction( product_id, correlation_id ) release_correction_id = release_correction.get('releaseCorrectionId') if not release_correction_id: logger.error(('Problem creating release' f'correction for product: {product_id}'), custom_fields={ 'raw_errors': release_correction['errors']}) raise ReleaseCorrectionError() return release_correction_id def delete_release_correction(product_id, release_correction_id, orchard_client, correlation_id, logger): """Delete a release correction.""" response = orchard_client.delete_release_correction( product_id, release_correction_id, correlation_id ) if is_error(response): logger.error((f'Problem deleting' f'release correction {release_correction_id}' f'for product: {product_id}'), custom_fields={ 'raw_errors': response['errors']} ) raise ReleaseCorrectionError() logger.info((f'Deleted release correction {release_correction_id} ' f'for product: {product_id}')) def submit_product(product_id, vendor_id, subaccount_id, orchard_client, correlation_id, logger): """Submit a product.""" response = orchard_client.submit_product( product_id, correlation_id, vendor_id, subaccount_id ) if is_error(response): logger.error( (f'Problem calling submit_product for product: {product_id}'), custom_fields={ 'product_id': product_id, 'vendor_id': vendor_id, 'subaccount_id': subaccount_id, 'raw_errors': response['errors'] }) raise ProductSubmissionError() logger.info(f'Submitted product {product_id}.') def unsubmit_product(product_id, orchard_client, correlation_id, logger): """Unsubmit a product.""" response = orchard_client.unsubmit_product(product_id, correlation_id) if is_error(response): logger.error( ('Problem calling unsubmit_product for product: {}').format( product_id ), custom_fields={ 'product_id': product_id, 'raw_errors': response['errors'] }) raise ReleaseCorrectionError() logger.info(f'Unsubmitted product {product_id}.') def create_release_correction_detail(product_id, release_correction_id, release_correction_payload, orchard_client, correlation_id, logger): """Create a release correction detail.""" logger.info(('Creating release correction detail' f'for {release_correction_id}'), custom_fields={ 'payload': release_correction_payload} ) response = orchard_client.create_release_correction_detail( product_id, release_correction_id, release_correction_payload, correlation_id) if is_error(response): logger.error((f'Problem setting up release' f'correction detail for product: {product_id}'), custom_fields={ 'raw_errors': response['errors']} ) raise ReleaseCorrectionError() logger.info(f'Updated product {product_id} using release correction.') def construct_release_correction_product_payload(updates): release_correction_payload = [] for field_name, new_value in updates.items(): table_name, field_name = ( RELEASE_CORRECTION_PRODUCT_FIELDS[field_name]) release_correction_payload.append({ 'keyValue': new_value, 'tableName': table_name, 'fieldName': field_name }) return release_correction_payload def construct_release_correction_track_payload(updates): release_correction_payload = [] for key_id, track_updates in updates.items(): for track_update in track_updates: field_name, new_value = track_update table_name, field_name = ( RELEASE_CORRECTION_TRACK_FIELDS[field_name]) release_correction_payload.append({ 'keyId': int(key_id), 'keyValue': new_value, 'tableName': table_name, 'fieldName': field_name }) return release_correction_payload def update_non_release_correctable_fields(orchard_product, sony_product, sony_tracks, message, orchard_client, logger): """Update fields that are updatable but don't support rc""" # Format and update supported Product fields logger.info(('About to update product and track fields' ' that do not support release correction')) orchard_product_id = orchard_product['productId'] result_ids = format_result_id(orchard_product_id, message.sending_system_local_id) product_result = \ orchard_client.update_non_release_correctable_product_fields( format_update_product_input( orchard_product_id, sony_product, [] ), message.correlation_id ) if is_error(product_result): msg = ('Error updating product fields' ' that do not support release correction') logger.error(msg, custom_fields={ 'raw_errors': product_result['errors']} ) return create_processing_result( [message.sending_system_local_id] + result_ids, message, errors=[format_generic_error(msg)] ) for track in sony_tracks: # Use only the fields we are not allowed to update normally through RC supported_keys = [ 'recordingCountryId', 'originalRightsHolderCountryId', 'publishers', 'ownershipRights', 'samples', 'performers' ] payload = { 'body': { k: track[k] for k in supported_keys if k in track }, 'tracks': track['tuids'] } track_results = orchard_client.update_tracks( payload, message.correlation_id) if is_error(track_results): msg = ('Error updating track fields' ' that do not support release correction') logger.error(msg, custom_fields={ 'raw_errors': track_results['errors']} ) return create_processing_result( [message.sending_system_local_id] + result_ids, message, errors=[format_generic_error(msg)] ) def handle_release_correction_update(orchard_product, sony_product, product_updates, track_updates, message, orchard_client, logger): """ Update a completed product using release correction. """ release_correction_payload = [ *construct_release_correction_product_payload(product_updates), *construct_release_correction_track_payload(track_updates) ] release_correction_id = orchard_product.get('releaseCorrectionId') product_id = int(orchard_product['productId']) result_ids = format_result_id(product_id, message.sending_system_local_id) if not release_correction_payload: logger.info(f'No release correction updates for product {product_id}.') return create_processing_result( [message.sending_system_local_id] + result_ids, message ) try: if orchard_product.get('displayStatus') == SUBMITTED: unsubmit_product(product_id, orchard_client, message.correlation_id, logger) if not release_correction_id: logger.info( f'Creating release correction for product {product_id}.') release_correction_id = create_release_correction( product_id, orchard_client, message.correlation_id, logger) else: msg = (f'Existing release correction id: {release_correction_id}' ' attached to product. Reusing for creating items.') logger.info(msg) create_release_correction_detail(product_id, release_correction_id, release_correction_payload, orchard_client, message.correlation_id, logger) submit_product(product_id, orchard_product.get('vendorId'), orchard_product.get('subAccount'), orchard_client, message.correlation_id, logger) return create_processing_result( [message.sending_system_local_id] + result_ids, message, ) except ReleaseCorrectionError: # Using this exception to help cleanup the checking in this function logger.error(f'Could not update product {product_id} ' f'using release correction.') return create_processing_result( [message.sending_system_local_id] + result_ids, message, errors=[format_generic_error(FAILED_TO_UPDATE_PRODUCT_IN_RC)] ) except ProductSubmissionError: logger.error( f'Could not resubmit product from release correction {product_id}.' ) send_unsubmit_email(orchard_product, sony_product, True) return create_processing_result( [message.sending_system_local_id] + result_ids, message, errors=[ format_generic_error(FAILED_TO_RESUBMIT_PRODUCT.format( product_id )) ] )