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
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
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
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