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
Ejemplo n.º 4
0
    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])
Ejemplo n.º 6
0
    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
Ejemplo n.º 9
0
    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
Ejemplo n.º 13
0
    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
Ejemplo n.º 14
0
    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
Ejemplo n.º 15
0
    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