"""Pricing Helper.""" import copy import uuid import asyncio from datetime import datetime, timedelta from oto import response from pricing import config from pricing.connectors import s3 from pricing.constants import pricing_family from pricing.logic import track_pricing_override from pricing.models import default_account_pricing_tier from pricing.models import orchard_pricing_tier from pricing.models import ows_product from pricing.models import product_orchard_pricing_tier from pricing.models import product_pricing_override from pricing.models import token all_resolutions = set(['SD', 'HD']) def apply_pricing_element_to_list( pricing_element_to_apply, pricing_elements_target, use_overlapping_dates): """Apply a pricing element to a list. Args: pricing_element_to_apply (dict): a dict with a pricing element pricing_elements_target ([dict]): an array of dict with the pricing elements use_overlapping_dates (bool): whether date intervals should overlap Returns: an array of dict with the pricing elements """ result = [] for pricing_element_target in pricing_elements_target: result = result + apply_pricing_element_to_single_element( pricing_element_to_apply, pricing_element_target, use_overlapping_dates) return result def apply_pricing_element_to_single_element( pricing_element_to_apply, pricing_element_target, use_overlapping_dates): """Apply a pricing element to a single element. Args: pricing_element_to_apply (dict): a dict with a pricing element to apply pricing_element_target (dict): a dict with a pricing element to target use_overlapping_dates (bool): whether date intervals should overlap Returns: A dict with a pricing element. """ intersect_result = intersect( pricing_element_to_apply, pricing_element_target, use_overlapping_dates) result = intersect_result['subtract'] if 'intersect' in intersect_result: if 'price_code' in pricing_element_to_apply: intersect_result['intersect']['price_code'] = \ pricing_element_to_apply['price_code'] if 'custom_price' in pricing_element_to_apply: intersect_result['intersect']['custom_price'] = \ pricing_element_to_apply['custom_price'] if 'custom_currency_code' in pricing_element_to_apply: intersect_result['intersect']['custom_currency_code'] = \ pricing_element_to_apply['custom_currency_code'] if 'store_pricing_tier_name' in pricing_element_to_apply: intersect_result['intersect']['store_pricing_tier_name'] = \ pricing_element_to_apply['store_pricing_tier_name'] else: intersect_result['intersect']['store_pricing_tier_name'] = 'Custom' result.append(intersect_result['intersect']) return result def intersect( pricing_element_to_apply, pricing_element_target, use_overlapping_dates): """Split a pricing element intersecting and other element. Args: pricing_element_to_apply (dict): a dict with a pricing element to apply pricing_element_target (dict): a dict with a pricing element to target use_overlapping_dates (bool): whether date intervals should overlap Returns: A dict with a the intersection and the subtracted pricing elements. """ subtract = [] territory_result = split_on_territories( pricing_element_to_apply, pricing_element_target) if 'intersect' not in territory_result: return {'subtract': [pricing_element_target]} subtract += territory_result['subtract'] resolution_result = split_on_resolutions( pricing_element_to_apply, territory_result['intersect']) if 'intersect' not in resolution_result: return {'subtract': [pricing_element_target]} subtract += resolution_result['subtract'] date_result = split_on_dates( pricing_element_to_apply, resolution_result['intersect'], use_overlapping_dates) if 'intersect' not in date_result: return {'subtract': [pricing_element_target]} subtract += date_result['subtract'] if 'track_ids' in pricing_element_to_apply: track_ids_result = split_on_track_ids( pricing_element_to_apply, date_result['intersect']) if 'intersect' not in track_ids_result: return {'subtract': [pricing_element_target]} subtract += track_ids_result['subtract'] intersection_element = track_ids_result['intersect'] else: intersection_element = date_result['intersect'] return { 'subtract': subtract, 'intersect': intersection_element } def split_on_territories(pricing_element_to_apply, pricing_element_target): """Split territories upon a pricing element. Args: pricing_element_to_apply (dict): a dict with a pricing element to apply pricing_element_target (dict): a dict with a pricing element to target Returns: A dict with an intersect and a subtract pricing element """ if is_worldwide(pricing_element_to_apply): return { 'intersect': pricing_element_target, 'subtract': [] } result = {} if is_worldwide(pricing_element_target): pricing_element_intersect = copy.deepcopy(pricing_element_target) pricing_element_intersect['applies_worldwide'] = False pricing_element_intersect['territory_list_include'] = \ pricing_element_to_apply['territory_list_include'] pricing_element_intersect['territories'] = pricing_element_to_apply[ 'territories'] result['intersect'] = pricing_element_intersect pricing_element_subtract = copy.deepcopy(pricing_element_target) pricing_element_subtract['territories'] = pricing_element_to_apply[ 'territories'] pricing_element_subtract['territory_list_include'] = \ not pricing_element_to_apply['territory_list_include'] pricing_element_subtract['applies_worldwide'] = False result['subtract'] = [pricing_element_subtract] return result intersect_territory_list_include = True subtract_territory_list_include = True target_territories = set(pricing_element_target['territories']) apply_territories = set(pricing_element_to_apply['territories']) if pricing_element_target['territory_list_include'] and \ pricing_element_to_apply['territory_list_include']: territories_intersect = target_territories & apply_territories territories_subtract = target_territories - apply_territories elif pricing_element_target['territory_list_include'] and \ not pricing_element_to_apply['territory_list_include']: territories_intersect = target_territories - apply_territories territories_subtract = target_territories & apply_territories elif not pricing_element_target['territory_list_include'] and \ pricing_element_to_apply['territory_list_include']: territories_intersect = apply_territories - target_territories territories_subtract = target_territories | apply_territories subtract_territory_list_include = False else: territories_intersect = apply_territories | target_territories intersect_territory_list_include = False territories_subtract = apply_territories - target_territories if not territories_intersect: return { 'subtract': [pricing_element_target] } if not territories_subtract: return { 'intersect': pricing_element_target, 'subtract': [] } pricing_element_intersect = copy.deepcopy(pricing_element_target) pricing_element_intersect['territories'] = list(territories_intersect) pricing_element_intersect['territory_list_include'] = \ intersect_territory_list_include result['intersect'] = pricing_element_intersect pricing_element_subtract = copy.deepcopy(pricing_element_target) pricing_element_subtract['territories'] = list(territories_subtract) pricing_element_subtract['territory_list_include'] = \ subtract_territory_list_include result['subtract'] = [pricing_element_subtract] return result def is_worldwide(pricing_element): """Check if a pricing element applies worldwide. Args: pricing_element (dict): a dict with a pricing element Returns: boolean indicating whether pricing element applies worldwide """ return 'applies_worldwide' in pricing_element and \ pricing_element['applies_worldwide'] def split_on_resolutions(pricing_element_to_apply, pricing_element_target): """Split resolutions upon a pricing element. Args: pricing_element_to_apply (dict): a dict with a pricing element to apply pricing_element_target (dict): a dict with a pricing element to target Returns: A dict with an intersect and a subtract pricing element """ if 'resolution' not in pricing_element_to_apply: return { 'intersect': pricing_element_target, 'subtract': [] } if 'resolution' not in pricing_element_target: return { 'intersect': pricing_element_target, 'subtract': [] } result = {} resolution_apply = preprocess_resolution( pricing_element_to_apply['resolution']) resolution_target = preprocess_resolution( pricing_element_target['resolution']) resolutions_subtract = resolution_target - resolution_apply resolutions_intersect = resolution_target & resolution_apply if not resolutions_intersect: return { 'subtract': [pricing_element_target] } if not resolutions_subtract: return { 'intersect': pricing_element_target, 'subtract': [] } pricing_element_intersect = copy.deepcopy(pricing_element_target) pricing_element_intersect['resolution'] = postprocess_resolutions( resolutions_intersect) result['intersect'] = pricing_element_intersect pricing_element_subtract = copy.deepcopy(pricing_element_target) pricing_element_subtract['resolution'] = postprocess_resolutions( resolutions_subtract) result['subtract'] = [pricing_element_subtract] return result def preprocess_resolution(resolution): """Pre process resolution for a pricing element. Args: resolution (string): A resolution e.g. SD or HD Returns: A set with resolutions """ if resolution == 'All': return all_resolutions return set([resolution]) def postprocess_resolutions(resolutions): """Post process resolutions for a pricing element. Args: resolutions (set[resolutions]): A set of resolutions array Returns: The first element of the resolutions set """ return resolutions.pop() def split_on_dates( pricing_element_to_apply, pricing_element_target, use_overlapping_dates): """Split dates upon a pricing element. Args: pricing_element_to_apply (dict): a dict with a pricing element to apply pricing_element_target (dict): a dict with a pricing element to target use_overlapping_dates (bool): whether date intervals should overlap Returns: A dict with a pricing element """ convert_and_default_dates(pricing_element_to_apply) convert_and_default_dates(pricing_element_target) if pricing_element_to_apply['start_date'] > \ pricing_element_target['end_date']: convert_and_un_default_dates(pricing_element_to_apply) convert_and_un_default_dates(pricing_element_target) return { 'subtract': [pricing_element_target] } if pricing_element_to_apply['end_date'] < \ pricing_element_target['start_date']: convert_and_un_default_dates(pricing_element_to_apply) convert_and_un_default_dates(pricing_element_target) return { 'subtract': [pricing_element_target] } if use_overlapping_dates: before_end_date = pricing_element_to_apply['start_date'] else: before_end_date = subtract_one_day( pricing_element_to_apply['start_date']) before_interval = { 'start_date': pricing_element_target['start_date'], 'end_date': before_end_date } intersect_interval = { 'start_date': date_max( pricing_element_to_apply['start_date'], pricing_element_target['start_date']), 'end_date': date_min( pricing_element_to_apply['end_date'], pricing_element_target['end_date']) } if use_overlapping_dates: after_start_date = pricing_element_to_apply['end_date'] else: after_start_date = add_one_day(pricing_element_to_apply['end_date']) after_interval = { 'start_date': after_start_date, 'end_date': pricing_element_target['end_date'] } convert_and_un_default_dates(pricing_element_to_apply) convert_and_un_default_dates(pricing_element_target) result = { 'subtract': [] } before_element = create_interval_element( before_interval, pricing_element_target) if before_element: result['subtract'].append(before_element) after_element = create_interval_element( after_interval, pricing_element_target) if after_element: result['subtract'].append(after_element) intersect_element = create_interval_element( intersect_interval, pricing_element_target) if intersect_element: result['intersect'] = intersect_element return result def date_min(date1, date2): """Get minimum of two datetimes. Args: date1 (datetime): the first date date2 (datetime): the second date Returns: the earliest of the two dates """ if date1 < date2: return date1 return date2 def date_max(date1, date2): """Get maximum of two datetimes. Args: date1 (datetime): the first date date2 (datetime): the second date Returns: the latest of the two dates """ if date1 > date2: return date1 return date2 def subtract_one_day(date): """Subtract one day. Args: date (datetime): the date to subtract from Returns: the date with one day subtracted """ if date == datetime.min: return date return date - timedelta(days=1) def add_one_day(date): """Add one day. Args: date (datetime): the date to add to Returns: the date with one day added """ if date == datetime.max: return date return date + timedelta(days=1) def split_on_track_ids(pricing_element_to_apply, pricing_element_target): """Split track_ids upon a pricing element. Args: pricing_element_to_apply (dict): a dict with a pricing element to apply pricing_element_target (dict): a dict with a pricing element to target Returns: A dict with an intersect and a subtract pricing element """ result = {} track_ids_apply = set(pricing_element_to_apply['track_ids']) track_ids_target = set(pricing_element_target['track_ids']) track_ids_subtract = track_ids_target - track_ids_apply track_ids_intersect = track_ids_target & track_ids_apply if not track_ids_intersect: return { 'subtract': [pricing_element_target] } if not track_ids_subtract: return { 'intersect': pricing_element_target, 'subtract': [] } pricing_element_intersect = copy.deepcopy(pricing_element_target) pricing_element_intersect['track_ids'] = list(track_ids_intersect) result['intersect'] = pricing_element_intersect pricing_element_subtract = copy.deepcopy(pricing_element_target) pricing_element_subtract['track_ids'] = list(track_ids_subtract) result['subtract'] = [pricing_element_subtract] return result def create_interval_element(interval, source_element): """Create an interval pricing element from a source element. Args: interval (dict): a dict with a pricing interval source_element (dict): a dict with a pricing element clone from Returns: A dict with a pricing element """ if interval['start_date'] >= interval['end_date']: return None result = copy.deepcopy(source_element) if interval['start_date'] != datetime.min: result['start_date'] = interval['start_date'] if interval['end_date'] != datetime.max: result['end_date'] = interval['end_date'] return result def convert_and_default_dates(pricing_element): """Set default dates on a pricing element and convert string to datetime. Args: pricing_element (dict): a dict with a pricing element """ if 'start_date' not in pricing_element \ or pricing_element['start_date'] is None: pricing_element['start_date'] = datetime.min elif isinstance(pricing_element['start_date'], float): pricing_element['start_date'] = datetime.fromtimestamp( pricing_element['start_date']) elif isinstance(pricing_element['start_date'], str): pricing_element['start_date'] = datetime.strptime( pricing_element['start_date'], '%Y-%m-%d') if 'end_date' not in pricing_element \ or pricing_element['end_date'] is None: pricing_element['end_date'] = datetime.max elif isinstance(pricing_element['end_date'], float): pricing_element['end_date'] = datetime.fromtimestamp( pricing_element['end_date']) elif isinstance(pricing_element['end_date'], str): pricing_element['end_date'] = datetime.strptime( pricing_element['end_date'], '%Y-%m-%d') def convert_and_un_default_dates(pricing_element): """Remove default dates on an element and convert datetime to string. Args: pricing_element (dict): a dict with a pricing element """ if pricing_element['start_date'] == datetime.min: del pricing_element['start_date'] if pricing_element['end_date'] == datetime.max: del pricing_element['end_date'] def get_default_account_pricing_tier(product_id, pricing_family_id): """Get the default pricing tier by account of product and family_id. Args: product_id (int): the product_id (release_id) of the product pricing_family_id (int): the ID of the pricing family. Returns: response.Response: containing the default account pricing tier dict. """ product_details_response = ows_product.get_product_details(product_id) if not product_details_response: return response.create_not_found_response() vendor_id = product_details_response.message.get('vendor_id') subaccount_id = product_details_response.message.get('subaccount_id') account_type = 'vendor' account_id = vendor_id if subaccount_id: account_type = 'subaccount' account_id = subaccount_id return default_account_pricing_tier.\ get_by_account_and_pricing_family_id( account_type, account_id, pricing_family_id) def _generate_filename(): unique_name = uuid.uuid4() underscored_name = str(unique_name).replace('-', '_') return f'ready_{underscored_name}.json' def get_upload_permission(user_id, duration): """Generate data required for pricing csv upload. Args: user_id (string): User id duration (int): Number of seconds upload permissions should live. Returns: response.Response: Contains information for uploading or error. """ if not user_id: return response.create_error_response( 'user_required', 'User is required') filename = _generate_filename() credentials = token.get_s3_token(filename, duration) if not credentials: return credentials permission = { 'bucket': f'{config.PRICING_BUCKET}/{config.PRICING_FOLDER}', 'filename': filename, 'credentials': credentials.message } return response.Response(permission) def get_all_digital_product_pricing(product_id): """Get all relevent pricing information for the digital frontend. Args: product_id (int): the ID of the product. Returns: flask.Response: The product pricing info as a dict. """ pricing_families_response = orchard_pricing_tier.\ get_orchard_pricing_tier_by_pricing_family_ids( (pricing_family.AUDIO_ALBUM_PRICING_FAMILY_ID, pricing_family.AUDIO_TRACK_PRICING_FAMILY_ID)) album_pricing_tier_response = product_orchard_pricing_tier.\ get_by_product_id_and_pricing_family_id( product_id, pricing_family.AUDIO_ALBUM_PRICING_FAMILY_ID) track_pricing_tier_response = product_orchard_pricing_tier.\ get_by_product_id_and_pricing_family_id( product_id, pricing_family.AUDIO_TRACK_PRICING_FAMILY_ID) album_pricing_override_response = product_pricing_override.\ get_by_product_id(product_id) track_pricing_override_response = track_pricing_override.\ get_track_pricing_overrides_for_product(product_id) bff_response = { 'pricing_tier_options': pricing_families_response.message, 'album_pricing_tier': album_pricing_tier_response.message, 'track_pricing_tier': track_pricing_tier_response.message, 'album_pricing_override': album_pricing_override_response.message, 'track_pricing_override': track_pricing_override_response.message } return response.Response(bff_response) def get_presigned_urls(parts): """Return presigned URLs for a number of parts, a filename.""" s3_client = s3.get_s3_client() filename = f'{config.PRICING_FOLDER}/{_generate_filename()}' result = s3_client.create_multipart_upload( Bucket=config.PRICING_BUCKET, Key=filename, ) upload_token = result['UploadId'] def get_part_presigned_url(part_number: int): return { 'part_number': part_number, 'url': s3_client.generate_presigned_url( ClientMethod='upload_part', Params={ 'Bucket': config.PRICING_BUCKET, 'Key': filename, 'UploadId': upload_token, 'PartNumber': part_number, }, ), } presigned_urls = [get_part_presigned_url(p) for p in range(1, parts + 1)] return response.Response({ 'filename': filename, 'presigned_urls': presigned_urls, 'upload_token': upload_token, }) def complete_multipart_upload(data): """Complete metadata request""" s3_client = s3.get_s3_client() s3_client.complete_multipart_upload( Bucket=config.PRICING_BUCKET, Key=data['filename'], UploadId=data['upload_token'], MultipartUpload={ 'Parts': [ {'PartNumber': part['part_number'], 'ETag': part['etag']} for part in data['parts'] ] }, ) return response.Response({'status': 'success'})