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 _get_effective_pricing(self, item_id, product_price, offering_info): # Search the pricing chosen by the user def field_included(pricing, field): return field in pricing and len(pricing[field]) > 0 matches = 0 price = None for off_price in offering_info['productOfferingPrice']: # Change the price to string in order to avoid problems with floats product_price['price']['amount'] = unicode(product_price['price']['amount']) # Validate that all pricing fields matches if off_price['priceType'].lower() == product_price['priceType'].lower() and \ ((not field_included(off_price, 'unitOfMeasure') and not field_included(product_price, 'unitOfMeasure')) or (field_included(off_price, 'unitOfMeasure') and field_included(product_price, 'unitOfMeasure') and off_price['unitOfMeasure'].lower() == product_price['unitOfMeasure'].lower())) and \ ((not field_included(off_price, 'recurringChargePeriod') and not field_included(product_price, 'recurringChargePeriod')) or (field_included(off_price, 'recurringChargePeriod') and field_included(product_price, 'recurringChargePeriod') and off_price['recurringChargePeriod'].lower() == product_price['recurringChargePeriod'].lower())) and \ Decimal(off_price['price']['taxIncludedAmount']) == Decimal(product_price['price']['amount']) and \ off_price['price']['currencyCode'].lower() == product_price['price']['currency'].lower(): matches += 1 price = off_price if not matches: raise OrderingError('The product price included in orderItem ' + item_id + ' does not match with any of the prices included in the related offering') elif matches > 1: raise OrderingError('The product price included in orderItem ' + item_id + ' matches with multiple pricing models of the related offering') return price
def _parse_alteration(self, alteration, type_): # Alterations cannot specify usage models if alteration['priceType'].lower() != 'one time' and alteration['priceType'].lower() != 'recurring': raise OrderingError('Invalid priceType in price alteration, it must be one time or recurring') # Check if it is a fixed value or a percentage if 'percentage' in alteration['price'] and Decimal(alteration['price']['percentage']) > Decimal(0): value = alteration['price']['percentage'] else: value = { 'value': alteration['price']['taxIncludedAmount'], 'duty_free': alteration['price']['dutyFreeAmount'] } alt_model = { 'type': type_, 'value': value, 'period': alteration['priceType'].lower() } # Parse condition if 'priceCondition' in alteration and len(alteration['priceCondition']): exp = re.compile('^(eq|lt|gt|le|ge) \d+(\.\d+)?$') if not exp.match(alteration['priceCondition']): raise OrderingError('Invalid priceCondition in price alteration, format must be: [eq | lt | gt | le | ge] value') op, value = alteration['priceCondition'].split(' ') alt_model['condition'] = { 'operation': op, 'value': value } return alt_model
def _get_offering(self, item): # Download related product offering and product specification site = urlparse(settings.SITE) off = urlparse(item['productOffering']['href']) offering_url = '{}://{}{}'.format(site.scheme, site.netloc, off.path) offering_info = self._download(offering_url, 'product offering', item['id']) offering_id = offering_info['id'] # Check if the offering has been already loaded in the system if len(Offering.objects.filter(off_id=offering_id)) > 0: offering = Offering.objects.get(off_id=offering_id) # If the offering defines a digital product, check if the customer already owns it included_offerings = [Offering.objects.get(pk=ObjectId(off_pk)) for off_pk in offering.bundled_offerings] included_offerings.append(offering) for off in included_offerings: if off.is_digital and off.pk in self._customer.userprofile.current_organization.acquired_offerings: raise OrderingError('The customer already owns the digital product offering ' + off.name + ' with id ' + off.off_id) else: raise OrderingError('The offering ' + offering_id + ' has not been previously registered') return offering, offering_info
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 _process_add_items(self, items, order_id, description, terms_accepted): new_contracts = [self._build_contract(item) for item in items] terms_found = False for c in new_contracts: off = Offering.objects.get(pk=ObjectId(c.offering)) if off.asset is not None and off.asset.has_terms: terms_found = True if terms_found and not terms_accepted: raise OrderingError('You must accept the terms and conditions of the offering to acquire it') current_org = self._customer.userprofile.current_organization order = Order.objects.create( order_id=order_id, customer=self._customer, owner_organization=current_org, date=datetime.utcnow(), state='pending', tax_address=self._get_billing_address(items), contracts=new_contracts, description=description ) self.rollback_logger['models'].append(order) charging_engine = ChargingEngine(order) return charging_engine.resolve_charging()
def _get_offering(self, item): # Download related product offering and product specification offering_info = self._download(item['productOffering']['href'], 'product offering', item['id']) offering_id = offering_info['id'] # Check if the offering has been already loaded in the system if len(Offering.objects.filter(off_id=offering_id)) > 0: offering = Offering.objects.get(off_id=offering_id) # If the offering defines a digital product, check if the customer already owns it included_offerings = [ Offering.objects.get(pk=off_pk) for off_pk in offering.bundled_offerings ] included_offerings.append(offering) def owned_digital(off): if off.is_digital and off.pk in self._customer.userprofile.current_organization.acquired_offerings: raise OrderingError( 'The customer already owns the digital product offering ' + off.name + ' with id ' + off.off_id) return off map(owned_digital, included_offerings) else: raise OrderingError('The offering ' + offering_id + ' has not been previously registered') return offering, offering_info
def owned_digital(off): if off.is_digital and off.pk in self._customer.userprofile.current_organization.acquired_offerings: raise OrderingError( 'The customer already owns the digital product offering ' + off.name + ' with id ' + off.off_id) return off
def _get_billing_address(self, items): def _download_asset(url): headers = { 'Authorization': 'Bearer ' + self._customer.userprofile.access_token } if not self._customer.userprofile.current_organization.private: headers[ 'x-organization'] = self._customer.userprofile.current_organization.name r = requests.get(url, headers=headers, verify=settings.VERIFY_REQUESTS) if r.status_code != 200: raise OrderingError( 'There was an error at the time of retrieving the Billing Address' ) return r.json() site = urlparse(settings.SITE) billing_url = urlparse(items[0]['billingAccount'][0]['href']) billing_account = _download_asset('{}://{}{}'.format( site.scheme, site.netloc, billing_url.path)) customer_acc_url = urlparse(billing_account['customerAccount']['href']) customer_account = _download_asset('{}://{}{}'.format( site.scheme, site.netloc, customer_acc_url.path)) customer_url = urlparse(customer_account['customer']['href']) customer = _download_asset('{}://{}{}'.format(site.scheme, site.netloc, customer_url.path)) postal_addresses = [ contactMedium for contactMedium in customer['contactMedium'] if contactMedium['type'] == 'PostalAddress' ] if len(postal_addresses) != 1: raise OrderingError( 'Provided Billing Account does not contain a Postal Address') postal_address = postal_addresses[0]['medium'] return { 'street': postal_address['streetOne'] + '\n' + postal_address.get('streetTwo', ''), 'postal': postal_address['postcode'], 'city': postal_address['city'], 'province': postal_address['stateOrProvince'], 'country': postal_address['country'] }
def _download(self, url, element, item_id): r = requests.get(url, verify=settings.VERIFY_REQUESTS) if r.status_code != 200: raise OrderingError('The ' + element + ' specified in order item ' + item_id + ' does not exists') return r.json()
def _build_contract(self, item): # TODO: Check that the ordering API is actually validating that the chosen pricing and characteristics are valid for the given product # Build offering offering, offering_info = self._get_offering(item) # Build pricing if included pricing = {} if 'product' in item and 'productPrice' in item['product'] and len(item['product']['productPrice']): model_mapper = { 'one time': 'single_payment', 'recurring': 'subscription', 'usage': 'pay_per_use' } # The productPrice field in the orderItem does not contain all the needed # information (neither taxes nor alterations), so extract pricing from the offering price = self._get_effective_pricing(item['id'], item['product']['productPrice'][0], offering_info) price_unit = self._parse_price(model_mapper, price) pricing['general_currency'] = price['price']['currencyCode'] pricing[model_mapper[price['priceType'].lower()]] = [price_unit] # Process price alterations if 'productOfferPriceAlteration' in price: alteration = price['productOfferPriceAlteration'] # Check type of alteration (discount or fee) if 'discount' in alteration['name'].lower() and 'fee' not in alteration['name'].lower(): # Is a discount pricing['alteration'] = self._parse_alteration(alteration, 'discount') elif 'discount' not in alteration['name'].lower() and 'fee' in alteration['name'].lower(): # Is a fee if 'priceCondition' not in alteration or not len(alteration['priceCondition']): # In this case the alteration is processed as another price price_unit = self._parse_price(model_mapper, alteration) if model_mapper[alteration['priceType'].lower()] not in pricing: pricing[model_mapper[alteration['priceType'].lower()]] = [] pricing[model_mapper[alteration['priceType'].lower()]].append(price_unit) else: pricing['alteration'] = self._parse_alteration(alteration, 'fee') else: raise OrderingError('Invalid price alteration, it is not possible to determine if it is a discount or a fee') # Calculate the revenue sharing class revenue_class = offering_info['serviceCandidate']['id'] return Contract( item_id=item['id'], pricing_model=pricing, revenue_class=revenue_class, offering=offering )
def process_order(self, customer, order, terms_accepted=False): """ Process the different order items included in a given ordering depending on its action field :param customer: :param order: :return: """ self._customer = customer # Check initial state of the order. It must be Acknowledged if order['state'].lower() != 'acknowledged': raise OrderingError('Only acknowledged orders can be initially processed') # Classify order items by action items = { 'add': [], 'modify': [], 'delete': [], 'no_change': [] } for item in order['orderItem']: items[item['action'].lower()].append(item) if len(items['add']) and len(items['modify']): raise OrderingError('It is not possible to process add and modify items in the same order') # Process order items separately depending on its action. no_change items are not processed if len(items['delete']): self._process_delete_items(items['delete']) redirection_url = None if len(items['modify']): redirection_url = self._process_modify_items(items['modify']) # Process add items if len(items['add']): description = '' if 'description' in order: description = order['description'] redirection_url = self._process_add_items(items['add'], order['id'], description, terms_accepted) return redirection_url
def get_item_contract(self, item_id): # Search related contract for c in self.contracts: if c.item_id == item_id: contract = c break else: raise OrderingError('Invalid item id') return contract
def get_product_contract(self, product_id): # Search related contract for c in self.contracts: if c.product_id == product_id: contract = c break else: raise OrderingError('Invalid product id') return contract
def _execute_renovation_transactions(self, transactions, err_msg): if len(transactions): # Make the charge redirect_url = self._charge_client(transactions) self._save_pending_charge(transactions) else: # If it is not necessary to charge the customer, the state is set to paid self._order.state = 'paid' self._order.save() raise OrderingError(err_msg) return redirect_url
def _download_asset(url): headers = { 'Authorization': 'Bearer ' + self._customer.userprofile.access_token } if not self._customer.userprofile.current_organization.private: headers['x-organization'] = self._customer.userprofile.current_organization.name r = requests.get(url, headers=headers, verify=settings.VERIFY_REQUESTS) if r.status_code != 200: raise OrderingError('There was an error at the time of retrieving the Billing Address') return r.json()
def _download_asset(url): r = requests.get(url, headers={ 'Authorization': 'Bearer ' + self._customer.userprofile.access_token }) if r.status_code != 200: raise OrderingError( 'There was an error at the time of retrieving the Billing Address' ) return r.json()
def _parse_price(self, model_mapper, price): if price['priceType'].lower() not in model_mapper: raise OrderingError('Invalid price model ' + price['priceType']) unit_field = { 'one time': 'priceType', 'recurring': 'recurringChargePeriod', 'usage': 'unitOfMeasure' } return { 'value': price['price']['taxIncludedAmount'], 'unit': price[unit_field[price['priceType'].lower()]].lower(), 'tax_rate': price['price']['taxRate'], 'duty_free': price['price']['dutyFreeAmount'] }
def _get_billing_address(self, items): def _download_asset(url): r = requests.get(url, headers={ 'Authorization': 'Bearer ' + self._customer.userprofile.access_token }) if r.status_code != 200: raise OrderingError( 'There was an error at the time of retrieving the Billing Address' ) return r.json() billing_account = _download_asset( items[0]['billingAccount'][0]['href']) customer_account = _download_asset( billing_account['customerAccount']['href']) customer = _download_asset(customer_account['customer']['href']) postal_addresses = [ contactMedium for contactMedium in customer['contactMedium'] if contactMedium['type'] == 'PostalAddress' ] if len(postal_addresses) != 1: raise OrderingError( 'Provided Billing Account does not contain a Postal Address') postal_address = postal_addresses[0]['medium'] return { 'street': postal_address['streetOne'] + '\n' + postal_address.get('streetTwo', ''), 'postal': postal_address['postcode'], 'city': postal_address['city'], 'province': postal_address['stateOrProvince'], 'country': postal_address['country'] }
def _get_existing_contract(self, inv_client, product_id): # Get product info raw_product = inv_client.get_product(product_id) # Get related order order = Order.objects.get(order_id=raw_product['name'].split('=')[1]) # Get the existing contract contract = order.get_product_contract(product_id) # TODO: Process pay per use case if 'subscription' in contract.pricing_model: # Check if there are a pending subscription now = datetime.utcnow() for subs in contract.pricing_model['subscription']: timedelta = subs['renovation_date'] - now if timedelta.days > 0: raise OrderingError('You cannot modify a product with a recurring payment until the subscription expires') return order, contract