예제 #1
0
    def get_transaction_parameters(self,
                                   basket,
                                   request=None,
                                   use_client_side_checkout=False,
                                   **kwargs):
        """
        Create a new PayPal payment.

        Arguments:
            basket (Basket): The basket of products being purchased.
            request (Request, optional): A Request object which is used to construct PayPal's `return_url`.
            use_client_side_checkout (bool, optional): This value is not used.
            **kwargs: Additional parameters; not used by this method.

        Returns:
            dict: PayPal-specific parameters required to complete a transaction. Must contain a URL
                to which users can be directed in order to approve a newly created payment.

        Raises:
            GatewayError: Indicates a general error or unexpected behavior on the part of PayPal which prevented
                a payment from being created.
        """
        # PayPal requires that item names be at most 127 characters long.
        PAYPAL_FREE_FORM_FIELD_MAX_SIZE = 127
        return_url = urljoin(get_ecommerce_url(), reverse('paypal:execute'))
        data = {
            'intent':
            'sale',
            'redirect_urls': {
                'return_url': return_url,
                'cancel_url': self.cancel_url,
            },
            'payer': {
                'payment_method': 'paypal',
            },
            'transactions': [{
                'amount': {
                    'total': str(basket.total_incl_tax),
                    'currency': basket.currency,
                },
                # Paypal allows us to send additional transaction related data in 'description' & 'custom' field
                # Free form field, max length 127 characters
                # description : program_id:<program_id>
                'description':
                "program_id:{}".format(get_basket_program_uuid(basket)),
                'item_list': {
                    'items': [
                        {
                            'quantity':
                            line.quantity,
                            # PayPal requires that item names be at most 127 characters long.
                            # for courseid we're using 'name' field along with title,
                            # concatenated field will be 'courseid|title'
                            'name':
                            middle_truncate(self.get_courseid_title(line),
                                            PAYPAL_FREE_FORM_FIELD_MAX_SIZE),
                            # PayPal requires that the sum of all the item prices (where price = price * quantity)
                            # equals to the total amount set in amount['total'].
                            'price':
                            str(line.line_price_incl_tax_incl_discounts /
                                line.quantity),
                            'currency':
                            line.stockrecord.price_currency,
                        } for line in basket.all_lines()
                    ],
                },
                'invoice_number':
                basket.order_number,
            }],
        }

        if waffle.switch_is_active('create_and_set_webprofile'):
            locale_code = self.resolve_paypal_locale(
                request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME))
            web_profile_id = self.create_temporary_web_profile(locale_code)
            if web_profile_id is not None:
                data['experience_profile_id'] = web_profile_id
        else:
            try:
                web_profile = PaypalWebProfile.objects.get(
                    name=self.DEFAULT_PROFILE_NAME)
                data['experience_profile_id'] = web_profile.id
            except PaypalWebProfile.DoesNotExist:
                pass

        available_attempts = 1
        if waffle.switch_is_active('PAYPAL_RETRY_ATTEMPTS'):
            available_attempts = self.retry_attempts

        for i in range(1, available_attempts + 1):
            try:
                payment = paypalrestsdk.Payment(data, api=self.paypal_api)
                payment.create()
                if payment.success():
                    break
                if i < available_attempts:
                    logger.warning(
                        u"Creating PayPal payment for basket [%d] was unsuccessful. Will retry.",
                        basket.id,
                        exc_info=True)
                else:
                    error = self._get_error(payment)
                    # pylint: disable=unsubscriptable-object
                    entry = self.record_processor_response(
                        error, transaction_id=error['debug_id'], basket=basket)
                    logger.error(u"%s [%d], %s [%d].",
                                 "Failed to create PayPal payment for basket",
                                 basket.id,
                                 "PayPal's response recorded in entry",
                                 entry.id,
                                 exc_info=True)
                    raise GatewayError(error)

            except:  # pylint: disable=bare-except
                if i < available_attempts:
                    logger.warning(
                        u"Creating PayPal payment for basket [%d] resulted in an exception. Will retry.",
                        basket.id,
                        exc_info=True)
                else:
                    logger.exception(
                        u"After %d retries, creating PayPal payment for basket [%d] still experienced exception.",
                        i, basket.id)
                    raise

        entry = self.record_processor_response(payment.to_dict(),
                                               transaction_id=payment.id,
                                               basket=basket)
        logger.info(
            "Successfully created PayPal payment [%s] for basket [%d].",
            payment.id, basket.id)

        for link in payment.links:
            if link.rel == 'approval_url':
                approval_url = link.href
                break
        else:
            logger.error(
                "Approval URL missing from PayPal payment [%s]. PayPal's response was recorded in entry [%d].",
                payment.id, entry.id)
            raise GatewayError(
                'Approval URL missing from PayPal payment response. See entry [{}] for details.'
                .format(entry.id))

        parameters = {
            'payment_page_url': approval_url,
        }

        return parameters
예제 #2
0
    def _generate_parameters(self, basket, use_sop_profile, **kwargs):
        """ Generates the parameters dict.

        A signature is NOT included in the parameters.

         Arguments:
            basket (Basket): Basket from which the pricing and item details are pulled.
            use_sop_profile (bool, optional): Indicates if the Silent Order POST profile should be used.
            **kwargs: Additional parameters to add to the generated dict.

         Returns:
             dict: Dictionary containing the payment parameters that should be sent to CyberSource.
        """
        site = basket.site

        access_key = self.access_key
        profile_id = self.profile_id

        if use_sop_profile:
            access_key = self.sop_access_key
            profile_id = self.sop_profile_id

        parameters = {
            'access_key':
            access_key,
            'profile_id':
            profile_id,
            'transaction_uuid':
            uuid.uuid4().hex,
            'signed_field_names':
            '',
            'unsigned_field_names':
            '',
            'signed_date_time':
            datetime.datetime.utcnow().strftime(ISO_8601_FORMAT),
            'locale':
            self.language_code,
            'transaction_type':
            'sale',
            'reference_number':
            basket.order_number,
            'amount':
            str(basket.total_incl_tax),
            'currency':
            basket.currency,
            'override_custom_receipt_page':
            get_receipt_page_url(
                site_configuration=site.siteconfiguration,
                order_number=basket.order_number,
                override_url=site.siteconfiguration.build_ecommerce_url(
                    reverse('cybersource:redirect')),
                disable_back_button=True,
            ),
            'override_custom_cancel_page':
            self.cancel_page_url,
        }
        extra_data = []
        # Level 2/3 details
        if self.send_level_2_3_details:
            parameters['amex_data_taa1'] = site.name
            parameters['purchasing_level'] = '3'
            parameters['line_item_count'] = basket.all_lines().count()
            # Note (CCB): This field (purchase order) is required for Visa;
            # but, is not actually used by us/exposed on the order form.
            parameters['user_po'] = 'BLANK'

            # Add a parameter specifying the basket's program, None if not present.
            # This program UUID will *always* be in the merchant_defined_data1, if exists.
            program_uuid = get_basket_program_uuid(basket)
            if program_uuid:
                extra_data.append(
                    "program,{program_uuid}".format(program_uuid=program_uuid))
            else:
                extra_data.append(None)

            for index, line in enumerate(basket.all_lines()):
                parameters['item_{}_code'.format(
                    index)] = line.product.get_product_class().slug
                parameters['item_{}_discount_amount '.format(index)] = str(
                    line.discount_value)
                # Note (CCB): This indicates that the total_amount field below includes tax.
                parameters['item_{}_gross_net_indicator'.format(index)] = 'Y'
                parameters['item_{}_name'.format(index)] = clean_field_value(
                    line.product.title)
                parameters['item_{}_quantity'.format(index)] = line.quantity
                parameters['item_{}_sku'.format(
                    index)] = line.stockrecord.partner_sku
                parameters['item_{}_tax_amount'.format(index)] = str(
                    line.line_tax)
                parameters['item_{}_tax_rate'.format(index)] = '0'
                parameters['item_{}_total_amount '.format(index)] = str(
                    line.line_price_incl_tax_incl_discounts)
                # Note (CCB): Course seat is not a unit of measure. Use item (ITM).
                parameters['item_{}_unit_of_measure'.format(index)] = 'ITM'
                parameters['item_{}_unit_price'.format(index)] = str(
                    line.unit_price_incl_tax)

                # For each basket line having a course product, add course_id and course type
                # as an extra CSV-formatted parameter sent to Cybersource.
                # These extra course parameters will be in parameters merchant_defined_data2+.
                line_course = line.product.course
                if line_course:
                    extra_data.append(
                        "course,{course_id},{course_type}".format(
                            course_id=line_course.id if line_course else None,
                            course_type=line_course.type
                            if line_course else None))

        # Only send consumer_id for hosted payment page
        if not use_sop_profile:
            parameters['consumer_id'] = basket.owner.username

        # Add the extra parameters
        parameters.update(kwargs.get('extra_parameters', {}))

        # Mitigate PCI compliance issues
        signed_field_names = list(parameters.keys())
        if any(pci_field in signed_field_names
               for pci_field in self.PCI_FIELDS):
            raise PCIViolation(
                'One or more PCI-related fields is contained in the payment parameters. '
                'This service is NOT PCI-compliant! Deactivate this service immediately!'
            )

        if extra_data:
            # CyberSource allows us to send additional data in merchant_defined_data# fields.
            for num, item in enumerate(extra_data, start=1):
                if item:
                    key = u"merchant_defined_data{num}".format(num=num)
                    parameters[key] = item

        return parameters
예제 #3
0
    def authorize_payment_api(self, transient_token_jwt, basket, request,
                              form_data):
        clientReferenceInformation = Ptsv2paymentsClientReferenceInformation(
            code=basket.order_number, )
        processingInformation = Ptsv2paymentsProcessingInformation(
            capture=True,
            purchase_level="3",
        )
        tokenInformation = Ptsv2paymentsTokenInformation(
            transient_token_jwt=transient_token_jwt, )
        orderInformationAmountDetails = Ptsv2paymentsOrderInformationAmountDetails(
            total_amount=str(basket.total_incl_tax),
            currency=basket.currency,
        )

        orderInformationBillTo = Ptsv2paymentsOrderInformationBillTo(
            first_name=form_data['first_name'],
            last_name=form_data['last_name'],
            address1=form_data['address_line1'],
            address2=form_data['address_line2'],
            locality=form_data['city'],
            administrative_area=form_data['state'],
            postal_code=form_data['postal_code'],
            country=form_data['country'],
            email=request.user.email,
        )

        merchantDefinedInformation = []
        program_uuid = get_basket_program_uuid(basket)
        if program_uuid:
            programInfo = Ptsv2paymentsMerchantDefinedInformation(
                key="1",
                value="program,{program_uuid}".format(
                    program_uuid=program_uuid))
            merchantDefinedInformation.append(programInfo.__dict__)

        merchantDataIndex = 2
        orderInformationLineItems = []
        for line in basket.all_lines():
            orderInformationLineItem = Ptsv2paymentsOrderInformationLineItems(
                product_name=clean_field_value(line.product.title),
                product_code=line.product.get_product_class().slug,
                product_sku=line.stockrecord.partner_sku,
                quantity=line.quantity,
                unit_price=str(line.unit_price_incl_tax),
                total_amount=str(line.line_price_incl_tax_incl_discounts),
                unit_of_measure='ITM',
                discount_amount=str(line.discount_value),
                discount_applied=True,
                amount_includes_tax=True,
                tax_amount=str(line.line_tax),
                tax_rate='0',
            )
            orderInformationLineItems.append(orderInformationLineItem.__dict__)
            line_course = line.product.course
            if line_course:
                courseInfo = Ptsv2paymentsMerchantDefinedInformation(
                    key=str(merchantDataIndex),
                    value="course,{course_id},{course_type}".format(
                        course_id=line_course.id if line_course else None,
                        course_type=line_course.type if line_course else None))
                merchantDefinedInformation.append(courseInfo.__dict__)
                merchantDataIndex += 1

        orderInformationInvoiceDetails = Ptsv2paymentsOrderInformationInvoiceDetails(
            purchase_order_number='BLANK')

        orderInformation = Ptsv2paymentsOrderInformation(
            amount_details=orderInformationAmountDetails.__dict__,
            bill_to=orderInformationBillTo.__dict__,
            line_items=orderInformationLineItems,
            invoice_details=orderInformationInvoiceDetails.__dict__)

        requestObj = CreatePaymentRequest(
            client_reference_information=clientReferenceInformation.__dict__,
            processing_information=processingInformation.__dict__,
            token_information=tokenInformation.__dict__,
            order_information=orderInformation.__dict__,
            merchant_defined_information=merchantDefinedInformation)

        requestObj = del_none(requestObj.__dict__)
        requestObj = json.dumps(requestObj)

        api_instance = PaymentsApi(self.cybersource_api_config)
        payment_processor_response, _, _ = api_instance.create_payment(
            requestObj)

        # Add the billing address to the response so it's available for the rest of the order completion process
        payment_processor_response.billing_address = BillingAddress(
            first_name=form_data['first_name'],
            last_name=form_data['last_name'],
            line1=form_data['address_line1'],
            line2=form_data['address_line2'],
            line4=form_data['city'],
            postcode=form_data['postal_code'],
            state=form_data['state'],
            country=Country.objects.get(iso_3166_1_a2=form_data['country']))
        return payment_processor_response
예제 #4
0
    def authorize_payment_api(self, transient_token_jwt, basket, request,
                              form_data):
        clientReferenceInformation = Ptsv2paymentsClientReferenceInformation(
            code=basket.order_number, )
        processingInformation = Ptsv2paymentsProcessingInformation(
            capture=True,
            purchase_level="3",
        )
        tokenInformation = Ptsv2paymentsTokenInformation(
            transient_token_jwt=transient_token_jwt, )
        orderInformationAmountDetails = Ptsv2paymentsOrderInformationAmountDetails(
            total_amount=str(basket.total_incl_tax),
            currency=basket.currency,
        )

        orderInformationBillTo = Ptsv2paymentsOrderInformationBillTo(
            first_name=form_data['first_name'],
            last_name=form_data['last_name'],
            address1=form_data['address_line1'],
            address2=form_data['address_line2'],
            locality=form_data['city'],
            administrative_area=form_data['state'],
            postal_code=form_data['postal_code'],
            country=form_data['country'],
            email=request.user.email,
        )

        merchantDefinedInformation = []
        program_uuid = get_basket_program_uuid(basket)
        if program_uuid:
            programInfo = Ptsv2paymentsMerchantDefinedInformation(
                key="1",
                value="program,{program_uuid}".format(
                    program_uuid=program_uuid))
            merchantDefinedInformation.append(programInfo.__dict__)

        merchantDataIndex = 2
        orderInformationLineItems = []
        for line in basket.all_lines():
            orderInformationLineItem = Ptsv2paymentsOrderInformationLineItems(
                product_name=clean_field_value(line.product.title),
                product_code=line.product.get_product_class().slug,
                product_sku=line.stockrecord.partner_sku,
                quantity=line.quantity,
                unit_price=str(line.unit_price_incl_tax),
                total_amount=str(line.line_price_incl_tax_incl_discounts),
                unit_of_measure='ITM',
                discount_amount=str(line.discount_value),
                discount_applied=True,
                amount_includes_tax=True,
                tax_amount=str(line.line_tax),
                tax_rate='0',
            )
            orderInformationLineItems.append(orderInformationLineItem.__dict__)
            line_course = line.product.course
            if line_course:
                courseInfo = Ptsv2paymentsMerchantDefinedInformation(
                    key=str(merchantDataIndex),
                    value="course,{course_id},{course_type}".format(
                        course_id=line_course.id if line_course else None,
                        course_type=line_course.type if line_course else None))
                merchantDefinedInformation.append(courseInfo.__dict__)
                merchantDataIndex += 1

        orderInformationInvoiceDetails = Ptsv2paymentsOrderInformationInvoiceDetails(
            purchase_order_number='BLANK')

        orderInformation = Ptsv2paymentsOrderInformation(
            amount_details=orderInformationAmountDetails.__dict__,
            bill_to=orderInformationBillTo.__dict__,
            line_items=orderInformationLineItems,
            invoice_details=orderInformationInvoiceDetails.__dict__)

        requestObj = CreatePaymentRequest(
            client_reference_information=clientReferenceInformation.__dict__,
            processing_information=processingInformation.__dict__,
            token_information=tokenInformation.__dict__,
            order_information=orderInformation.__dict__,
            merchant_defined_information=merchantDefinedInformation)

        requestObj = del_none(requestObj.__dict__)

        # HACK: log the processor request into the processor response model for analyzing declines
        self.record_processor_response(requestObj,
                                       transaction_id='[REQUEST]',
                                       basket=basket)

        api_instance = PaymentsApi(self.cybersource_api_config)
        payment_processor_response, _, _ = api_instance.create_payment(
            json.dumps(requestObj),
            _request_timeout=(self.connect_timeout, self.read_timeout))

        # Add the billing address to the response so it's available for the rest of the order completion process
        payment_processor_response.billing_address = BillingAddress(
            first_name=form_data['first_name'],
            last_name=form_data['last_name'],
            line1=form_data['address_line1'],
            line2=form_data['address_line2'],
            line4=form_data['city'],
            postcode=form_data['postal_code'],
            state=form_data['state'],
            country=Country.objects.get(iso_3166_1_a2=form_data['country']))
        decoded_payment_token = None
        for _, decoded_capture_context in self._unexpired_capture_contexts(
                request.session):
            jwk = RSAAlgorithm.from_jwk(
                json.dumps(decoded_capture_context['flx']['jwk']))
            # We don't know which capture context was used for this payment token, so just try all unexpired ones
            try:
                decoded_payment_token = jwt.decode(transient_token_jwt,
                                                   key=jwk,
                                                   algorithms=['RS256'])
            except jwt.exceptions.InvalidSignatureError:
                continue
            else:
                break

        if decoded_payment_token is None:
            # Couldn't find a capture context that is valid for this payment token
            raise InvalidSignatureError()

        payment_processor_response.decoded_payment_token = decoded_payment_token
        return payment_processor_response