def create(self, request): try: event = json.loads(request.body) except: return build_response( request, 400, 'The provided data is not a valid JSON object') if event['eventType'] != 'ProductCreationNotification': return build_response(request, 200, 'OK') product = event['event']['product'] # Extract order id order_id = product['name'].split('=')[1] # Get order order = Order.objects.get(order_id=order_id) contract = None # Search contract for cont in order.contracts: if product['productOffering']['id'] == cont.offering.off_id: contract = cont if contract is None: return build_response( request, 404, 'There is not a contract for the specified product') # Save contract id contract.product_id = product['id'] order.save() # Activate asset try: on_product_acquired(order, contract) except: return build_response(request, 400, 'The asset has failed to be activated') # Change product state to active inventory_client = InventoryClient() inventory_client.activate_product(product['id']) # Create the initial charge in the billing API if len(contract.charges) == 1: billing_client = BillingClient() valid_to = None # If the initial charge was a subscription is needed to determine the expiration date if 'subscription' in contract.pricing_model: valid_to = contract.pricing_model['subscription'][0][ 'renovation_date'] billing_client.create_charge(contract.charges[0], contract.product_id, start_date=None, end_date=valid_to) return build_response(request, 200, 'OK')
def create(self, request): task, order, contract, error_response = validate_product_job( self, request) if error_response is not None: return error_response # If the model is pay-per-use charge for pending payment redirect_url = None if task['priceType'].lower() == 'usage': # The update of the product status need to be postponed if there is a pending payment redirect_url, error_response = process_product_payment( self, request, task, order, contract) if error_response is not None: return error_response response = build_response(request, 200, 'OK') # Include redirection header if needed if redirect_url is not None: response['X-Redirect-URL'] = redirect_url else: # Suspend the product as no pending payment on_product_suspended(order, contract) contract.suspended = True order.save() client = InventoryClient() client.suspend_product(contract.product_id) return response
def _process_delete_items(self, items): for item in items: if 'product' not in item: raise OrderingError( 'It is required to specify product information in delete order items' ) product = item['product'] if 'id' not in product: raise OrderingError( 'It is required to provide product id in delete order items' ) # Set the contract as terminated client = InventoryClient() order, contract = self._get_existing_contract( client, product['id']) # Suspend the access to the service on_product_suspended(order, contract) contract.terminated = True order.save() # Terminate product in the inventory client.terminate_product(product['id'])
def _set_renovation_states(self, transactions, raw_order, order): inventory_client = InventoryClient() for transaction in transactions: try: contract = order.get_item_contract(transaction['item']) inventory_client.activate_product(contract.product_id) except: pass
def _set_renovation_states(self, transactions, raw_order, order): inventory_client = InventoryClient() for transaction in transactions: try: contract = order.get_item_contract(transaction['item']) inventory_client.activate_product(contract.product_id) # Activate the product on_product_acquired(order, contract) except: pass
def ready(self): import sys from django.conf import settings from django.core.exceptions import ImproperlyConfigured from wstore.models import Context from wstore.store_commons.utils.url import is_valid_url from wstore.ordering.inventory_client import InventoryClient from wstore.rss_adaptor.rss_manager import ProviderManager # Creates a new user profile when an user is created # post_save.connect(create_user_profile, sender=User) register_signals() testing = sys.argv[1:2] == ['test'] or sys.argv[1:2] == ['migrate'] if not testing: # Validate that a correct site and local_site has been provided if not is_valid_url(settings.SITE) or not is_valid_url(settings.LOCAL_SITE): raise ImproperlyConfigured('SITE and LOCAL_SITE settings must be a valid URL') # Create context object if it does not exists if not len(Context.objects.all()): Context.objects.create( failed_cdrs=[], failed_upgrades=[] ) inventory = InventoryClient() inventory.create_inventory_subscription() # Create RSS default aggregator and provider credentials = { 'user': settings.STORE_NAME, 'roles': [settings.ADMIN_ROLE], 'email': settings.WSTOREMAIL } prov_manager = ProviderManager(credentials) try: prov_manager.register_aggregator({ 'aggregatorId': settings.WSTOREMAIL, 'aggregatorName': settings.STORE_NAME, 'defaultAggregator': True }) except Exception as e: # If the error is a conflict means that the aggregator is already registered if e.response.status_code != 409: raise e
def _process_modify_items(self, items): if len(items) > 1: raise OrderingError( 'Only a modify item is supported per order item') item = items[0] if 'product' not in item: raise OrderingError( 'It is required to specify product information in modify order items' ) product = item['product'] if 'id' not in product: raise OrderingError( 'It is required to provide product id in modify order items') client = InventoryClient() order, contract = self._get_existing_contract(client, product['id']) # Build the new contract new_contract = self._build_contract(item) if new_contract.pricing_model != {}: contract.pricing_model = new_contract.pricing_model contract.revenue_class = new_contract.revenue_class order.save() # The modified item is treated as an initial payment charging_engine = ChargingEngine(order) return charging_engine.resolve_charging(type_='initial', related_contracts=[contract])
def __init__(self, asset): Thread.__init__(self) self._asset = asset self._client = InventoryClient() # Get product name try: prod_url = '{}/api/catalogManagement/v2/productSpecification/{}?fields=name'\ .format(settings.CATALOG, self._asset.product_id) resp = requests.get(prod_url) resp.raise_for_status() self._product_name = resp.json()['name'] except HTTPError: self._product_name = None
def _check_renovation_date(self, renovation_date, order, contract): now = datetime.now() timed = renovation_date - now if timed.days < 7: handler = NotificationsHandler() if timed.days < 0: # Notify that the subscription has finished handler.send_payment_required_notification(order, contract) # Set the product as suspended client = InventoryClient() client.suspend_product(contract.product_id) else: # There is less than a week remaining handler.send_near_expiration_notification(order, contract, timed.days)
def _process_delete_items(self, items): for item in items: if 'product' not in item: raise OrderingError('It is required to specify product information in delete order items') product = item['product'] if 'id' not in product: raise OrderingError('It is required to provide product id in delete order items') # Set the contract as terminated client = InventoryClient() order, contract = self._get_existing_contract(client, product['id']) contract.terminated = True order.save() # Terminate product in the inventory client.terminate_product(product['id'])
def process_product_payment(self, request, task, order, contract): # Refresh accounting information on_usage_refreshed(order, contract) # Build charging engine charging_engine = ChargingEngine(order) redirect_url = None try: redirect_url = charging_engine.resolve_charging( type_=task['priceType'].lower(), related_contracts=[contract]) except ValueError as e: return None, build_response(request, 400, str(e)) except OrderingError as e: # The error might be raised because renewing a suspended product not expired if str( e ) == 'OrderingError: There is not recurring payments to renovate' and contract.suspended: try: on_product_acquired(order, contract) # Change product state to active contract.suspended = False order.save() inventory_client = InventoryClient() inventory_client.activate_product(contract.product_id) except: return None, build_response( request, 400, 'The asset has failed to be activated') else: return None, build_response(request, 422, str(e)) except: return None, build_response( request, 500, 'An unexpected event prevented your payment to be created') return redirect_url, None
def _check_renovation_date(self, renovation_date, order, contract): now = datetime.utcnow() timed = renovation_date - now if timed.days < 7: handler = NotificationsHandler() if timed.days < 0: # Suspend the access to the service on_product_suspended(order, contract) # Notify that the subscription has finished handler.send_payment_required_notification(order, contract) # Set the product as suspended client = InventoryClient() client.suspend_product(contract.product_id) else: # There is less than a week remaining handler.send_near_expiration_notification( order, contract, timed.days)
from __future__ import unicode_literals import sys from django.conf import settings from wstore.models import Context from wstore.ordering.inventory_client import InventoryClient from wstore.rss_adaptor.rss_manager import ProviderManager testing = sys.argv[1:2] == ['test'] if not testing and Context.objects.all(): inventory = InventoryClient() inventory.create_inventory_subscription() # Create RSS default aggregator and provider credentials = { 'user': settings.STORE_NAME, 'roles': ['provider'], 'email': settings.WSTOREMAIL } prov_manager = ProviderManager(credentials) try: prov_manager.register_aggregator({ 'aggregatorId': settings.WSTOREMAIL, 'aggregatorName': settings.STORE_NAME, 'defaultAggregator': True
from __future__ import unicode_literals import sys from django.conf import settings from wstore.models import Context from wstore.ordering.inventory_client import InventoryClient from wstore.rss_adaptor.rss_manager import ProviderManager testing = sys.argv[1:2] == ['test'] if not testing and Context.objects.all() and Context.objects.all( )[0].local_site is not None: inventory = InventoryClient() inventory.create_inventory_subscription() # Create RSS default aggregator and provider credentials = { 'user': settings.STORE_NAME, 'roles': ['provider'], 'email': settings.WSTOREMAIL } prov_manager = ProviderManager(credentials) try: prov_manager.register_aggregator({ 'aggregatorId': settings.WSTOREMAIL, 'aggregatorName': settings.STORE_NAME, 'defaultAggregator': True
class InventoryUpgrader(Thread): def __init__(self, asset): Thread.__init__(self) self._asset = asset self._client = InventoryClient() # Get product name try: prod_url = '{}/api/catalogManagement/v2/productSpecification/{}?fields=name'\ .format(settings.CATALOG, self._asset.product_id) resp = requests.get(prod_url) resp.raise_for_status() self._product_name = resp.json()['name'] except HTTPError: self._product_name = None def _save_failed(self, pending_off, pending_products): # The failed upgrades list may be upgraded by other threads or other server instances # In this case context must be accessed as a shared resource context_id = Context.objects.all()[0].pk lock = DocumentLock('wstore_context', context_id, 'ctx') lock.wait_document() # At this point only the current thread can modify the list of pending upgrades context = Context.objects.all()[0] context.failed_upgrades.append({ 'asset_id': self._asset.pk, 'pending_offerings': pending_off, 'pending_products': pending_products }) context.save() lock.unlock_document() def _notify_user(self, patched_product): if self._product_name is not None: try: not_handler = NotificationsHandler() order = Order.objects.get( order_id=patched_product['name'].split('=')[-1]) not_handler.send_product_upgraded_notification( order, order.get_product_contract(unicode(patched_product['id'])), self._product_name) except: # A failure in the email notification is not relevant pass def upgrade_products(self, product_ids, id_filter): def is_digital_char(characteristic): # Return whether a characteristics is defining asset info for the given one def is_product(id_): sp = id_.split(':') return len(sp) == 2 and sp[0] == 'product' and sp[ 1] == self._asset.product_id def is_offering(id_): sp = id_.split(':') offerings = [] if len(sp) == 2 and sp[0] == 'offering': offerings = Offering.objects.filter(off_id=sp[1]) return len(offerings) == 1 and \ (offerings[0].asset == self._asset or self._asset.pk in offerings[0].asset.bundled_assets) dig_char = False id_str = '' name = characteristic['name'].lower() if name.endswith('asset type') or name.endswith( 'media type') or name.endswith('location'): # There are several formats for asset characteristics within the inventory products depending on # the number and the structure of the involved bundles # name: Asset Type , For single offering with single product # name: offering:123 Asset Type , For bundle offering with single product # name: product:123 Asset Type , For single offering with bundle product # name: offering:123 product:345 Asset Type , For bundle offering with bundle product id_str = name.replace('asset type', '').replace('media type', '').replace('location', '') bundle_ids = id_str.split(' ') dig_char = len(bundle_ids) == 1 or \ (len(bundle_ids) == 2 and (is_product(bundle_ids[0]) or is_offering(bundle_ids[0]))) or \ (len(bundle_ids) == 3 and is_offering(bundle_ids[0]) and is_product(bundle_ids[1])) return dig_char, id_str n_pages = int(math.ceil(len(product_ids) / PAGE_LEN)) missing_upgrades = [] for page in range(0, n_pages): # Get the ids related to the current product page offset = page * int(PAGE_LEN) page_ids = [ unicode(id_filter(p_id)) for p_id in product_ids[offset:offset + int(PAGE_LEN)] ] ids = ','.join(page_ids) # Get product characteristics field try: products = self._client.get_products( query={ 'id': ids, 'fields': 'id,productCharacteristic' }) except HTTPError: missing_upgrades.extend(page_ids) continue # Patch product to include new asset information for product in products: pre_ids = '' product_id = unicode(product['id']) new_characteristics = [] for char in product['productCharacteristic']: is_dig, ids_str = is_digital_char(char) if not is_dig: new_characteristics.append(char) else: pre_ids = ids_str new_characteristics.append({ 'name': '{}Media Type'.format(pre_ids), 'value': self._asset.content_type }) new_characteristics.append({ 'name': '{}Asset Type'.format(pre_ids), 'value': self._asset.resource_type }) new_characteristics.append({ 'name': '{}Location'.format(pre_ids), 'value': self._asset.download_link }) try: # The inventory API returns the product after patching patched_product = self._client.patch_product( product_id, {'productCharacteristic': new_characteristics}) except HTTPError: missing_upgrades.append(product_id) continue self._notify_user(patched_product) return missing_upgrades def upgrade_asset_products(self, offering_ids): # Get all the product ids related to the given product offering missing_off = [] missing_products = [] for off_id in offering_ids: try: product_ids = self._client.get_products(query={ 'productOffering.id': off_id, 'fields': 'id' }) except HTTPError: # Failure reading the available product ids, all upgrades pending missing_off.append(off_id) continue missing_products.extend( self.upgrade_products(product_ids, lambda p_id: p_id['id'])) return missing_off, missing_products def _get_providing_offerings(self): # Get product bundles that contain the included asset assets = [self._asset] assets.extend(Resource.objects.filter(bundled_assets=self._asset.pk)) # Get all the offerings that include the asset or one of the bundles offerings = [] for asset in assets: offerings.extend(Offering.objects.filter(asset=asset)) # Get all the offering bundles which include the previous offerings bundles = [] for off in offerings: bundles.extend(Offering.objects.filter(bundled_offerings=off.pk)) offerings.extend(bundles) return offerings def run(self): # Get all the offerings that give access to the provided digital asset offerings = self._get_providing_offerings() # Upgrade all the products related to the provided asset missing_off, missing_products = self.upgrade_asset_products( [offering.off_id for offering in offerings]) if len(missing_off) > 0 or len(missing_products) > 0: self._save_failed(missing_off, missing_products)