"""Logic layer for product.""" from collections import defaultdict import datetime from itertools import groupby from operator import itemgetter from flask import current_app from flask import g from flask_executor_pde.executor import Executor from oto import response from product_digital.logic.validate_audio_product_feature_flag_context import ValidateAudioProductFeatureFlagContext from sqlalchemy import func as sql_func from product_digital import features from product_digital.connectors import mysql from product_digital.constants import account from product_digital.constants import error from product_digital.constants import error_correction from product_digital.constants import features as feature_names from product_digital.constants import product_statuses from product_digital.constants import pricing_family from product_digital.constants.product import IMMEDIATE_PREORDER_PLACEHOLDER_DATE, ValidationType from product_digital.logic import validation from product_digital.models import audio_product, neo4j_vendor from product_digital.models import mkt_priority from product_digital.models import ows_carveouts from product_digital.models import ows_pricing from product_digital.models import ows_product from product_digital.models import ows_product_physical from product_digital.models import ows_product_workflow from product_digital.models import ows_track from product_digital.models import product_events from product_digital.models import product_provided_store_artists from product_digital.models import project from product_digital.models import release from product_digital.models import release_artist from product_digital.models import release_status from product_digital.models import release_subgenre from product_digital.utils.dataloader_util import format_for_dataloader from product_digital.validation import audio_product as product_validation def create(product_data): """Create a new product. Args: product_data: a dictionary of product data used to create a new product Returns: Response: a response object """ project_id = product_data.get('project_id') account_type = product_data.get('account_type') account_id = product_data.get('account_id') if not account_type or not account_id: return response.create_error_response( code=error.VALIDATION_ERROR, message=error.ERROR_MESSAGE_NO_ACCOUNT_GIVEN, status=400) ownership_response = project.check_project_ownership( project_id, account_type, account_id) if not ownership_response: return ownership_response validation_response = product_validation.validate_product_on_create( product_data) if not validation_response: return validation_response product_response = _create_product_models(product_data) if not product_response: return product_response product_events.emit_digital_product_event( product_id=product_response.message['product_id'], upc=product_response.message['upc'], product_name=product_data.get('product_name'), operation_type=product_events.OperationType.CREATE, operation_context=product_events.OperationContext.NEW) return response.Response(status=201, message=product_response.message) def copy_product(product_id, fields): """Copy a product. Args: product_id (int): The product_id (release_id) of the product to copy. fields (dict): Fields that override original product fields when copying the product. Returns: Response: a response object """ existing_product_response = audio_product.get_product(product_id) if not existing_product_response: return existing_product_response new_product_data = existing_product_response.message new_product_data.update({ # Fields that should not be copied. 'product_id': None, # Equivalent to audio_product.release_id. 'upc': None, 'display_upc': None, 'product_code': None, 'delivered_version': None, 'format': None, 'version': None, }) new_product_data.update(fields) # Create a new product. Bypass ownership and validation checks. product_copy_response = _create_product_models(new_product_data) if not product_copy_response: return product_copy_response copy_product_message = product_copy_response.message # create release subgenre copy subgenre_response = copy_release_subgenre( product_id, copy_product_message) if subgenre_response: copy_product_message['subgenre_id'] = subgenre_response.message.get( 'subgenre_id') # create release artist copy release_artist_response = copy_release_artist( product_id, copy_product_message) if release_artist_response: release_artists = audio_product._format_product_artists( release_artist_response.message) copy_product_message['product_artists'] = release_artists product_events.emit_digital_product_event( product_id=copy_product_message['product_id'], upc=copy_product_message.get('upc'), product_name=fields.get('product_name'), operation_type=product_events.OperationType.CREATE, operation_context=product_events.OperationContext.COPY) return response.Response( status=201, message=_convert_dates_to_strings(copy_product_message) ) def copy_release_subgenre(source_release_id, dest_release_data): """Copy release subgenre from source release to destination release. Args: source_release_id (int): source release id dest_release_data (dict): destination release data Returns: response.Response: the result of copy operation """ source_subgenre = release_subgenre.get_release_subgenre_by_release_id( source_release_id) if not source_subgenre: return source_subgenre new_subgenre_data = source_subgenre.message new_subgenre_data.pop('release_subgenre_id', None) new_subgenre_data['release_id'] = dest_release_data['product_id'] new_subgenre_data['upc'] = dest_release_data.get('upc') new_subgenre = release_subgenre.save_release_subgenre(new_subgenre_data) return new_subgenre def update_audio_product(product_id, product_data, orchard_user_id=''): """Update the given product with the new values that are provided. Args: product_id (int): id of the product to update product_data (dict): new values to assign to the product orchard_user_id (str): Orchard user Id Returns: response.Response: the usual response object """ validation_response = ( product_validation.validate_product_on_update(product_data)) if not validation_response: return validation_response account_type = product_data.get('account_type') account_id = product_data.get('account_id') is_alw_user = orchard_user_id and orchard_user_id.startswith('alw:') if is_alw_user and (not account_type or not account_id): if not account_type or not account_id: return response.create_error_response( code=error.VALIDATION_ERROR, message=error.ERROR_MESSAGE_NO_ACCOUNT_GIVEN, status=400) project_id = product_data.get('project_id') if project_id: # TODO: Check existing project_id and skip this if project_id is same. if is_alw_user: ownership_response = project.check_project_ownership( project_id, account_type, account_id) if not ownership_response: return ownership_response new_project = project.get_project_by_id(project_id) if not new_project: return new_project new_subaccount_id = new_project.message['subaccount_id'] or None product_data['subaccount_id'] = new_subaccount_id product_data['artist_id'] = new_project.message['artist_id'] is_product_physical_supplychain_enabled = False if account_type == 'vendor' and account_id: is_product_physical_supplychain_enabled = features.is_feature_flag_enabled_for_vendor( feature_names.PRODUCT_PHYSICAL_SUPPLY_CHAIN, account_id) if is_product_physical_supplychain_enabled \ and product_data.get('context_type') == 'physical' \ and product_data.get('sale_start_date'): ows_product_physical.update_product_physical( product_data, product_id, orchard_user_id, account_type, account_id) product_response = audio_product.update( product_id, _convert_strings_to_dates(product_data), orchard_user_id=orchard_user_id, ) if product_response.status in (403, 404): return product_response if not product_response: error_message = 'failed to save product' if product_response.errors['message']: error_message = product_response.errors['message'] return response.create_fatal_response(error_message) product_events.emit_digital_product_event( product_id=product_response.message['product_id'], upc=product_response.message['upc'], product_name=product_response.message['product_name'], operation_type=product_events.OperationType.UPDATE) return response.Response( message=_convert_dates_to_strings(product_response.message)) def copy_release_artist(source_release_id, dest_release_data): """Copy release subgenre from source release to destination release. Args: source_release_id (int): source release id dest_release_data (dict): destination release data Returns: response.Response: the result of copy operation """ source_artists = release_artist.get_release_artists_by_release_id( source_release_id) if not source_artists: return source_artists source_artists = source_artists.message upc = dest_release_data.get('upc') dest_release_id = dest_release_data['product_id'] for artist_data in source_artists: artist_data.pop('release_artist_id', None) release_artists_response = release_artist.save_for_release_id( dest_release_id, upc, source_artists) return release_artists_response def product_submission_workflow( product_id, account_type, account_id, identity_id='', use_workflow_service=True): """Update a product's status. Update an existing product's `release_status` from 'label_processing' to 'transfer_to_content'. On success it will also insert a record to the `release_status` and `release_approval_queue` table. Args: product_id (int): id of the product to fetch account_type (str): the user account type in the header. account_id (int): the user account id in the header. Returns: response.Response: a response object indicating success or failure. """ with mysql.db_session() as session: product = release.get_release_instance(product_id, session) current_release_status = product.release_status if not product: return response.Response(status=404) if product.release_status == 'transfer_to_content': return response.Response( message=error.ERROR_MESSAGE_PRODUCT_ALREADY_SUBMITTED, status=400) project_response = project.get_project_by_id(product.project_id) if not project_response: return project_response workflow_response = None if use_workflow_service: workflow_response = get_product_workflow_response( product_id, account_type, account_id, identity_id=identity_id ) if workflow_response is not None and not workflow_response: return workflow_response if product.release_status == 'label_processing': product.release_status = product_statuses.TRANSFER_TO_CONTENT orchard_account_type = 'alw' if account_type == 'vendor' else 'oa' orchard_user_id = f'{orchard_account_type}:{account_id}' product_response = audio_product.format_submit_product( product, workflow_response, orchard_user_id=orchard_user_id ) product_response_message = product_response.message if current_release_status != product_statuses.IN_CONTENT: _create_release_status_entry( product_response_message.get('product_id'), product_response_message.get('release_status'), project_response.message.get('vendor_id'), 'vendor', session=session, ) return response.Response( message=_convert_dates_to_strings(product_response_message)) def get_product_workflow_response( product_id, account_type, account_id, identity_id=''): """Get product workflow response.""" return ows_product_workflow.product_submission( product_id, account_type, account_id, identity_id=identity_id) def submit_audio_product( product_id, account_type, account_id, vendor_id, orchard_user_id, ff_context, identity_id='', ): """Update a product's status. Update an existing product's `release_status` from 'label_processing' to 'transfer_to_content'. On success it will also insert a record to the `release_status` and `release_approval_queue` table. Args: product_id (int): id of the product to fetch account_type (str): the user account type in the header. account_id (int): the user account id in the header. vendor_id (int): the vendor id for the product. orchard_user_id (string): user id of an orchard user. ff_context (dict): feature flag context. identity_id (str): the identity id for the user. Returns: response.Response: a response object indicating success or failure. """ # Make sure that the product has all the required information. validation_response = validate_audio_product( product_id=product_id, account_type=account_type, account_id=account_id, vendor_id=vendor_id, orchard_user_id=orchard_user_id, ff_context=ff_context, ) if not validation_response: return validation_response validations = validation_response.message['validations'] for section_name in validations: section = validations[section_name] if not section['valid']: # Return the full set of validation failures. This enables # the frontend to update validation state for any sections # that are incomplete (specifically an issue when v2 artwork # is present and valid but v1 artwork is not). return response.create_error_response( code=error.VALIDATION_ERROR, message={ 'validations': validations }, status=400 ) product_submission_response = product_submission_workflow( product_id, account_type, account_id, identity_id=identity_id ) if not product_submission_response: return product_submission_response product_events.emit_digital_product_event( product_id=product_submission_response.message['product_id'], upc=product_submission_response.message['upc'], product_name=product_submission_response.message['product_name'], operation_type=product_events.OperationType.UPDATE, operation_context=product_events.OperationContext.SUBMIT) return product_submission_response def get_audio_product(product_id, orchard_user_id=''): """Fetch an audio product from a given product_id. Args: product_id (int): id of the product to fetch orchard_user_id (str): Orchard user Id Returns: response.Response: a response object with product information """ audio_product_response = ( audio_product.get_product(product_id, orchard_user_id)) if not audio_product_response: return audio_product_response fetched_product = _convert_dates_to_strings(audio_product_response.message) return response.Response(message=fetched_product) def get_audio_products(product_ids, orchard_user_id=''): """Fetch audio products from a given product_ids. Args: product_ids (list): ids of the product to fetch orchard_user_id (str): Orchard user Id Returns: response.Response: a response object with product information """ audio_products_response = ( audio_product.get_products(product_ids, orchard_user_id)) if not audio_products_response: return audio_products_response fetched_products = [_convert_dates_to_strings(product) for product in audio_products_response.message] return response.Response(message={ 'products': format_for_dataloader( fetched_products, sorted([product['product_id'] for product in fetched_products]), 'product_id', ) }) def dataload_product_audio_validation( product_ids, account_type, account_id, response_format, orchard_user_id='', validation_context=None, ): """Dataload the validation information for products.""" results = [] for product_id in product_ids: product_response = ows_product.get_product(product_id) if not product_response: return product_response vendor_id = product_response.message.get('vendor_id') ff_context=ValidateAudioProductFeatureFlagContext(vendor_id=vendor_id) resp = validate_audio_product( product_id, account_type, account_id, vendor_id, orchard_user_id, validation_context, ff_context, ) if not resp: return resp if response_format == 'list': final_output = validation.format_as_list(resp) results.append(final_output.message) else: results.append(resp.message) return response.Response(message=results) def validate_audio_product( product_id, account_type, account_id, vendor_id, orchard_user_id='', validation_context=None, ff_context=None, ): """Fetch an audio product and validate it. Args: product_id (int): id of the product to fetch account_type (str): the user account type in the header. account_id (int): the user account id in the header. vendor_id (int): the vendor id for the product. orchard_user_id (string): user id of an orchard user. validation_context (str): Indicate pre_submission or post_submission. ff_context (dict): feature flag context. Returns: response.Response: a response object with validation errors if any. """ call_and_args_list = [ ( get_audio_product, ( product_id, orchard_user_id ) ), ( product_validation.validate_artwork, (product_id,) ), ( product_validation.validate_tracks, ( product_id, orchard_user_id ) ), ( product_validation.validate_publishing_obligation, ( product_id, account_type, account_id, orchard_user_id ) ), ] if ff_context.spatial_upc_validation: call_and_args_list.append(( product_validation.validate_spatial_upc, ( product_id, orchard_user_id, account_type, account_id, ) )) if validation_context == 'post_submission': call_and_args_list.extend([ ( product_validation.validate_product_blocklist, (product_id,) ), ( product_validation.validate_account, (product_id,) ) ]) with Executor(current_app) as executor: response_list = list(executor.map_multiple(call_and_args_list)) # Loop through each response and return first failure for response_item in response_list: if not response_item: return response_item product_data = response_list.pop(0).message tracks_data = product_validation.get_tracks_by_product_id(product_id) single_track_product = len(tracks_data) == 1 if 'corrections' in product_data: product_data = _apply_error_correction_fields_to_product(product_data) product_data['corrections_by_track_id'] = _format_track_correction_fields(product_data) validations_message = product_validation.validate_product_basics( product_data, tracks_data, single_track_product, ff_context ).message validations_message.update( product_validation.validate_product_dates(product_data).message) if validation_context == 'post_submission': latest_awal_artist = '' latest_awal_artist = neo4j_vendor.get_recent_awal_artist(vendor_id) validations_message.update( product_validation.validate_product_artists( product_data, tracks_data, latest_awal_artist, ff_context).message) validations_message.update( product_validation.validate_artwork_compliance( product_data, tracks_data, ff_context).message) if ff_context.preorder_pricing_warning and product_data.get('preorder_date'): validations_message[ValidationType.SCHEDULING_AND_PRICING]['warnings'] = \ product_validation.get_validate_pricing_warnings(product_id) for response_item in response_list: validations_message.update(response_item.message) return response.Response(message={'validations': validations_message}) def get_product_scheduling_and_pricing(product_id): """Fetch product scheduling and pricing information. Args: product_id (int): id of the product to fetch info about Returns: response.Response: a response object containing the pricing info """ product_response = release.get_release(product_id) if not product_response: return product_response product_data = _convert_dates_to_strings(product_response.message) scheduling_pricing_data = { 'sale_start_date': product_data.get('sale_start_date'), 'release_date': product_data.get('release_date'), 'preorder_date': product_data.get('preorder_date'), 'previewable': product_data.get('itunes_previewable')} album_pricing_tiers_response = ows_pricing.get_pricing_tiers( pricing_family.ALBUM) if not album_pricing_tiers_response: return album_pricing_tiers_response album_pricing_tiers = album_pricing_tiers_response.message.get('items') track_tiers_response = ows_pricing.get_pricing_tiers( pricing_family.TRACK) if not track_tiers_response: return track_tiers_response track_pricing_tiers = track_tiers_response.message.get('items') product_tier_response = ows_pricing.get_pricing_tier_by_product( product_id, pricing_family.ALBUM) if not product_tier_response: return product_tier_response scheduling_pricing_data.update({'album_pricing_tier': { 'name': product_tier_response.message.get('name'), 'id': product_tier_response.message.get('orchard_pricing_tier_id')}}) track_tier_response = ows_pricing.get_pricing_tier_by_product( product_id, pricing_family.TRACK) if not track_tier_response: return track_tier_response scheduling_pricing_data.update({'track_pricing_tier': { 'name': track_tier_response.message.get('name'), 'id': track_tier_response.message.get('orchard_pricing_tier_id')}}) album_overrides_response = ( ows_pricing.get_active_product_overrides(product_id)) if not album_overrides_response: return album_overrides_response album_overrides = list(map( _convert_pricing_overrides_unix_date_to_string, album_overrides_response.message.get('items'))) product_tier_data = _create_orchard_pricing_tier_dict( album_pricing_tiers) product_tier_data.update(_create_orchard_pricing_tier_dict( track_pricing_tiers)) for album in album_overrides: tier_name = product_tier_data[album['orchard_pricing_tier_id']]['name'] album.update({'orchard_pricing_tier_name': tier_name}) scheduling_pricing_data.update({'album_override_items': album_overrides}) track_overrides_response = ows_pricing.get_track_overrides(product_id) if not track_overrides_response: return track_overrides_response track_overrides = list(map( _convert_pricing_overrides_unix_date_to_string, track_overrides_response.message.get('items'))) track_tier_data = _create_orchard_pricing_tier_dict( track_pricing_tiers) for track in track_overrides: tier_name = track_tier_data[track['orchard_pricing_tier_id']]['name'] track.update({'orchard_pricing_tier_name': tier_name}) scheduling_pricing_data.update({'track_override_items': track_overrides}) return response.Response(message=scheduling_pricing_data) def get_product_instant_grats(product_id): """Get grats and related data and return formatted response. Args: product_id (int): id of the product to fetch grats about. Returns: response.Response: with a prepared for frontend grats data. """ instant_grats_response = ows_track.get_instant_grats_by_product_id( product_id) if not instant_grats_response: return instant_grats_response grats_data = instant_grats_response.message tracks_data = {} for instant_grat in grats_data.get('items'): track_response = ows_track.get_track_by_track_id( track_id=instant_grat.get('tuid'), ) if not track_response: return track_response track_data = track_response.message tracks_data[track_data['tuid']] = track_data stores_response = ows_carveouts.get_instant_grats_stores() if not stores_response: return stores_response stores_data = stores_response.message return _format_instant_grats_response( grats_data=grats_data, tracks_data=tracks_data, stores_data=stores_data) def is_product_submitted(product_id): """Validate if product is already submitted. Args: product_id (int): primary key of an audio product Returns: response.Response: status 400 if product already submitted. """ product_response = release.get_release(product_id) if not product_response: return product_response product_data = product_response.message is_product_already_submitted = product_data\ .get('release_status') == 'transfer_to_content' if is_product_already_submitted: return response.Response( message=error.ERROR_MESSAGE_PRODUCT_ALREADY_SUBMITTED, status=400) return response.Response() def get_mkt_priority(product_id): """Fetch an mkt priority from a given product_id. Args: product_id (int): id of the product to fetch Returns: response.Response: a response object with mkt priority information """ mkt_priority_response = mkt_priority.get_mkt_priority_by_release_id( product_id, ) if not mkt_priority_response: return mkt_priority_response return response.Response(message={'mkt_priority': mkt_priority_response.message}) def get_bulk_mkt_priority(product_ids): """Fetch bulk mkt priorities for given product_ids. Args: product_id (list): ids of the product to fetch Returns: response.Response: a response object with mkt priority information """ mkt_priority_response = mkt_priority.get_bulk_mkt_priority_by_release_ids( product_ids, ) if not mkt_priority_response: return mkt_priority_response return response.Response(message={ 'mkt_priority': format_for_dataloader( mkt_priority_response.message, sorted([mkt_p['id'] for mkt_p in mkt_priority_response.message]), 'id', ) }) def set_mkt_priority(product_id, country_id, priority, user_id): """Set mkt priority for a given product_id. Args: product_id (int): id of the product to fetch country_id (int): id of the country priority (str): a or b on None Returns: response.Response: a response object with mkt priority information """ return mkt_priority.set_mkt_priority_by_release_id( product_id, country_id, priority, user_id ) def delete_mkt_priority(product_id, country_id): """Set mkt priority for a given product_id. Args: product_id (int): id of the product to fetch country_id (int): id of the country Returns: response.Response: a response object with mkt priority information """ return mkt_priority.delete_mkt_priority_by_release_id( product_id, country_id, ) @mysql.wrap_db_errors def unsubmit_audio_product(product_id, account_id): """Unsubmit a product. Update an existing product's `release_status` from 'transfer_to_content' to 'label_processing' or reset its correction status from submitted to active. It will delete the latest record in the `release_approval_queue` table. It will insert a new record in the `release_status` table reflecting the current state of the product. Args: product_id (int): id of the product to fetch account_id (int): the user account id in the header. Returns: response.Response: a response object indicating success or failure. """ with mysql.db_session() as session: product = release.get_release_instance(product_id, session) if not product: return response.Response(status=404) if product.release_status not in ['transfer_to_content', 'in_content']: return response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message=error.ERROR_MESSAGE_PRODUCT_NOT_SUBMITTED ) workflow_response = ows_product_workflow.product_unsubmission( product_id, account_id, 'vendor') if not workflow_response and workflow_response.status != 404: return workflow_response if product.release_status == 'transfer_to_content': product.release_status = 'label_processing' _create_release_status_entry(product.release_id, product.release_status, account_id, 'vendor', session=session) product_events.emit_digital_product_event( product_id=product_id, operation_type=product_events.OperationType.UPDATE, operation_context=product_events.OperationContext.UNSUBMIT) return response.Response(message='product unsubmitted') @mysql.wrap_db_errors def reject_audio_product_from_productreview(product_id): """Reset a product to 'label_processing' OR set error_correction status to 'active'. If the product is in error correction set the status from 'submitted' to 'active' Else update an existing product's `release_status` from 'transfer_to_content' to 'label_processing' when rejecting it via ows-product-review. It will NOT delete the latest record in the `release_approval_queue` table. It will insert a new record in the `release_status` table reflecting the current state of the product. Args: product_id (int): id of the product to fetch Returns: response.Response: a response object indicating success or failure. """ with mysql.db_session() as session: product = release.get_release_instance(product_id, session) if not product: return response.Response(status=404) release_status = product.release_status correction_id = None if release_status == product_statuses.IN_CONTENT: correction = ows_product_workflow.get_last_release_correction(product_id) if correction.status not in (200, 404): return correction if correction.message.get('status') == product_statuses.SUBMITTED: correction_id = correction.message['release_correction_id'] # Product is not in submitted state or in error correction if release_status != product_statuses.TRANSFER_TO_CONTENT and correction_id is None: return response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message=error.ERROR_MESSAGE_PRODUCT_NOT_SUBMITTED ) if correction_id is not None: ows_workflow_response = ows_product_workflow.update_release_correction( correction_id, { 'status': product_statuses.ACTIVE, 'last_updated_by': 179, 'last_updated_type': 'oa' } ) if ows_workflow_response.status != 200: return ows_workflow_response else: product.release_status = product_statuses.LABEL_PROCESSING _create_release_status_entry( product.release_id, product.release_status, account.SYSTEM_USER_ID, 'oa', session=session, ) return response.Response(message='product rejected') @mysql.wrap_db_errors def approve_audio_product_from_productreview(product_id): """Set a product to 'in_content' and add ingestion_completed timestamp. Update an existing product's `release_status` from 'transfer_to_content' to 'in_content' when approving it via ows-product-review. It will NOT delete the latest record in the `release_approval_queue` table. It will insert a new record in the `release_status` table reflecting the current state of the product. Args: product_id (int): id of the product to fetch Returns: response.Response: a response object indicating success or failure. """ with mysql.db_session() as session: product = release.get_release_instance(product_id, session) if not product: return response.Response(status=404) if product.release_status != 'transfer_to_content': return response.create_error_response( code=error.ERROR_CODE_BAD_REQUEST, message=error.ERROR_MESSAGE_PRODUCT_NOT_SUBMITTED ) product.release_status = 'in_content' _create_release_status_entry( product.release_id, product.release_status, account.SYSTEM_USER_ID, 'oa', session=session, ) product.ingestion_completed = sql_func.now() if product.preorder_date == IMMEDIATE_PREORDER_PLACEHOLDER_DATE: product.preorder_date = sql_func.current_date() product_events.emit_digital_product_event( product_id=product_id, operation_type=product_events.OperationType.UPDATE, operation_context=product_events.OperationContext.APPROVE) return response.Response(message='product approved') @mysql.wrap_db_errors def update_not_for_distribution(product_id, not_for_distribution, release_status=None): """Update a product's not_for_distribution flag and product status. Args: product_id (int): id of the product to fetch not_for_distribution (str): the new value to set release_status (string): the new product status to set Returns: response.Response: a response object indicating success or failure. """ update_response = release.update_not_for_distribution( product_id, not_for_distribution, release_status ) if update_response.message.get('release_status', None): _create_release_status_entry( product_id, release_status, account.SYSTEM_USER_ID, 'oa' ) product_events.emit_digital_product_event( product_id=product_id, operation_type=product_events.OperationType.UPDATE) return update_response @mysql.wrap_db_errors def approve(product_id, changed_by, changed_by_type): """Update an existing product's `release_status` to 'in_content'. Args: product_id (int): id of the product to approve changed_by (int): id of account used. changed_by_type (string): system type making the change. Returns: response.Response: a response object indicating success or failure. """ approve_response = release.update_release_status( product_id, release.STATUS_IN_CONTENT ) _create_release_status_entry(product_id, release.STATUS_IN_CONTENT, changed_by, changed_by_type) product_events.emit_digital_product_event( product_id=product_id, operation_type=product_events.OperationType.UPDATE, operation_context=product_events.OperationContext.APPROVE) return approve_response @mysql.wrap_db_errors @mysql.db_session_wrap def delete(product_id, session, account_type=None, account_id=None): """Delete a product for the given product_id. Args: product_id (int): a product's release id session (db_session): Database session. account_type (str): a grass header parameter (subaccount|vendor). account_id (str): a grass header parameter. Returns: Response: A response object with the result of the delete request """ product_response = release.get_release(product_id, session=session, for_update=True) if not product_response: return product_response project_id = product_response.message.get('project_id') ownership_response = project.check_project_ownership( project_id, account_type, account_id) if not ownership_response: return ownership_response release_status = product_response.message.get('release_status') if release_status != product_statuses.LABEL_PROCESSING: error_message = 'Only products in `label_processing` can be deleted.' return response.create_error_response( message=error_message, code=error.ERROR_MESSAGE_INVALID_RELEASE_STATUS, status=400) return audio_product.delete(product_id, session) def delete_mkt_priority_for_product(product_id): """Delete mkt priority for a given product_id. Args: product_id (int): id of the product to delete Returns: response.Response: an empty response object """ return mkt_priority.delete_mkt_priority_for_product( product_id ) def _create_product_models(product_data): """Creates the database entries for a new product. Args: product_data: a dictionary of product data used to create a new product Returns: Response: a response object """ project_id = product_data.get('project_id') project_response = project.get_project_by_id(project_id) if not project_response: return project_response subaccount_id = product_data.get('subaccount_id')\ or project_response.message.get('subaccount_id') if subaccount_id: product_data['subaccount_id'] = subaccount_id final_product_data = { # Default values for non required fields. 'artist_id': project_response.message.get('artist_id'), 'distribution_format_id': 1, 'product_type_id': 1, 'vendor_catalog_number': project_response.message.get('project_code') } final_product_data.update(product_data) final_product_data.update({ # New products must have a label_processing status. 'release_status': 'label_processing', }) # If UPC is not assigned, get a UPC from provisioner queue and set as used if not final_product_data.get('upc'): provisioned_upc = _return_upc_value_and_update_status(True) g.ows.log.info( 'Using UPC assigned by UPC provisioner: {}'.format(provisioned_upc)) final_product_data.update({'upc': provisioned_upc}) else: g.ows.log.info('Using user-assigned UPC: {}'.format( final_product_data.get('upc'))) product_response = audio_product.create(final_product_data) if not product_response: return product_response _create_release_status_entry( product_response.message.get('product_id'), product_response.message.get('release_status'), project_response.message.get('vendor_id'), 'vendor', ) product_provided_store_artists.create(product_response.message.get('product_id')) return product_response def _map_error_correction_field_name(field_name): """Return the correct field name to be used on product data. Args: field_name (str): error correction field name """ if error_correction.FIELD_NAME_MAP.get(field_name): return error_correction.FIELD_NAME_MAP.get(field_name) return field_name def _apply_error_correction_fields_to_product(product_data): """Apply error correction fields to the product data. Certain fields are named or formatted differently in error correction. This performs all necessary translation and then applies them to the product data. Args: product_data (dict): the product data to modify """ release_corrections = [ item for item in product_data['corrections']['items'] if item['table_name'] == 'releases'] product_artists = [] corrected_roles = [] for correction in release_corrections: field_name = _map_error_correction_field_name(correction['field_name']) if correction['field_name'] == error_correction.RELEASE_SUBGENRE: product_data[field_name] = correction['key_value'][0] elif correction['field_name'] in error_correction.ARTIST_ROLES: corrected_roles.append(field_name) for artist in correction['key_value']: product_artists.append({ 'role': field_name, 'name': artist.get('artist_name') }) else: product_data[field_name] = correction['key_value'] if corrected_roles: original_product_artists = [ item for item in product_data[error_correction.PRODUCT_ARTISTS] if item.get('role') not in corrected_roles] product_data[error_correction.PRODUCT_ARTISTS] =\ original_product_artists + product_artists return product_data def _format_track_correction_fields(product_data): """Format track corrections by track id. Args: product_data (dict): the product data to modify """ corrections_by_track_id = defaultdict(dict) for item in product_data['corrections']['items']: if item['table_name'] == 'track': track_id = item['key_id'] corrections_by_track_id[track_id][item['field_name']] = item['key_value'] return corrections_by_track_id def _return_upc_value_and_update_status(mark_used): """Retrieves one message from ows_product and updates it to be used. Args: mark_used (boolean): update UPC status to used when True Returns: UPC str or None """ upc_response = ows_product.get_provisioned_upc(mark_used) if upc_response.status == 200: return upc_response.message return None def _convert_strings_to_dates(product_data): for field in ['preorder_date', 'release_date', 'sale_start_date']: if field in product_data and product_data[field]: if isinstance(product_data[field], datetime.date): continue product_data[field] = datetime.datetime.strptime( product_data[field], '%Y-%m-%d').date() return product_data def _convert_dates_to_strings(product_data): return { field: str(value) if isinstance(value, datetime.date) else value for field, value in product_data.items()} def _convert_pricing_overrides_unix_date_to_string(pricing_override): """Convert dates from pricing overrides to datetime.date.""" for field in ['start_date', 'end_date', 'created_date', 'updated_date']: if field in pricing_override and pricing_override[field]: pricing_override[field] = datetime.datetime.fromtimestamp( pricing_override[field]).strftime('%Y-%m-%d') return pricing_override def _create_orchard_pricing_tier_dict(seq): """Create a dictionary with 'orchard_pricing_tier_id' as key.""" return dict( ( d['orchard_pricing_tier_id'], dict(d, index=index) ) for (index, d) in enumerate(seq)) def _create_release_status_entry(release_id, status, changed_by, changed_by_type, session=None): """Create a record in the release_status table. Args: release_id (int): Release ID. status (str): Release status. changed_by (int): An integer representing the user who changed the status. changed_by_type (str): A string representing the type of user who changed the status. session (optional): A database session object. Defaults to None. """ enum_change_by_type = ['oa', 'vendor', 'system'] if changed_by_type not in enum_change_by_type: raise ValueError( f"Invalid changed_by_type, must be one of: {', '.join(enum_change_by_type)}") release_status_data = { 'release_id': release_id, 'status': status, 'changed_by': changed_by, 'changed_by_type': changed_by_type, } release_status.create(release_status_data, session=session) def _format_instant_grats_response(grats_data, tracks_data, stores_data): """Create formatted instant grats response from grats and tracks data. Args: grats_data (dict): Instant Grats basic data taken from ows-track GET /grats endpoint. tracks_data (dict): Mapping {'track_id': 'track_data'} contains additional tracks data. stores_data (dict): Mapping {'store_id': 'store_name'} contains stores data. Returns: response.Response: .message contains formatted Instant Grats response. """ temp_grats = [] for track_grats in grats_data['items']: tuid = track_grats.get('tuid') for grat in track_grats.get('grats'): new_grat = { 'tuid': tuid, 'date': grat['date'], 'store_id': grat['store_id'] } temp_grats.append(new_grat) grouper = itemgetter('tuid', 'date') formatted_grats = [] for key, grp in groupby(sorted(temp_grats, key=grouper), grouper): tuid, date = key grat_dict = { 'track': { 'tuid': tuid, 'track_name': tracks_data[tuid]['track_name']}, 'date': date, 'stores': [ {'store_id': item['store_id'], 'store_name': stores_data[str(item['store_id'])]} for item in grp]} formatted_grats.append(grat_dict) result = {'items': formatted_grats} return response.Response(result)