def form_valid(self, form): data = form.cleaned_data basket = data['basket'] request = self.request user = request.user logger.info('Valid payment form submitted for basket [%d].', basket.id) # Ensure we aren't attempting to purchase a basket that has already been purchased, frozen, # or merged with another basket. if basket.status != Basket.OPEN: logger.error( 'Basket %d must be in the "Open" state. It is currently in the "%s" state.', basket.id, basket.status) error_msg = _( 'Your basket may have been modified or already purchased. Refresh the page to try again.' ) return self._basket_error_response(error_msg) basket.strategy = request.strategy Applicator().apply(basket, user, self.request) # Add extra parameters for Silent Order POST extra_parameters = { 'payment_method': 'card', 'unsigned_field_names': ','.join(Cybersource.PCI_FIELDS), 'bill_to_email': user.email, 'device_fingerprint_id': request.session.session_key, } for source, destination in six.iteritems(self.FIELD_MAPPINGS): extra_parameters[destination] = clean_field_value(data[source]) parameters = Cybersource(self.request.site).get_transaction_parameters( basket, use_client_side_checkout=True, extra_parameters=extra_parameters) logger.info( 'Parameters signed for CyberSource transaction [%s], associated with basket [%d].', parameters.get('transaction_id'), basket.id) # This parameter is only used by the Web/Mobile flow. It is not needed for for Silent Order POST. parameters.pop('payment_page_url', None) # Ensure that the response can be properly rendered so that we # don't have to deal with thawing the basket in the event of an error. response = JsonResponse({'form_fields': parameters}) # Freeze the basket since the user is paying for it now. basket.freeze() return response
def form_valid(self, form): data = form.cleaned_data basket = data['basket'] request = self.request user = request.user sdn_check_failure = self.check_sdn(request, data) if sdn_check_failure is not None: return sdn_check_failure # Add extra parameters for Silent Order POST extra_parameters = { 'payment_method': 'card', 'unsigned_field_names': ','.join(Cybersource.PCI_FIELDS), 'bill_to_email': user.email, # Fall back to order number when there is no session key (JWT auth) 'device_fingerprint_id': request.session.session_key or basket.order_number, } for source, destination in self.FIELD_MAPPINGS.items(): extra_parameters[destination] = clean_field_value(data[source]) parameters = Cybersource(self.request.site).get_transaction_parameters( basket, use_client_side_checkout=True, extra_parameters=extra_parameters) logger.info( 'Parameters signed for CyberSource transaction [%s], associated with basket [%d].', # TODO: transaction_id is None in logs. This should be fixed. parameters.get('transaction_id'), basket.id) # This parameter is only used by the Web/Mobile flow. It is not needed for for Silent Order POST. parameters.pop('payment_page_url', None) # Ensure that the response can be properly rendered so that we # don't have to deal with thawing the basket in the event of an error. response = JsonResponse({'form_fields': parameters}) basket_add_organization_attribute(basket, data) # Freeze the basket since the user is paying for it now. basket.freeze() return response
def form_valid(self, form): data = form.cleaned_data basket = data['basket'] request = self.request user = request.user # Ensure we aren't attempting to purchase a basket that has already been purchased, frozen, # or merged with another basket. if basket.status != Basket.OPEN: logger.debug('Basket %d must be in the "Open" state. It is currently in the "%s" state.', basket.id, basket.status) error_msg = _('Your basket may have been modified or already purchased. Refresh the page to try again.') return self._basket_error_response(error_msg) basket.strategy = request.strategy Applicator().apply(basket, user, self.request) # Add extra parameters for Silent Order POST extra_parameters = { 'payment_method': 'card', 'unsigned_field_names': ','.join(Cybersource.PCI_FIELDS), 'bill_to_email': user.email, 'device_fingerprint_id': request.session.session_key, } for source, destination in six.iteritems(self.FIELD_MAPPINGS): extra_parameters[destination] = clean_field_value(data[source]) parameters = Cybersource(self.request.site).get_transaction_parameters( basket, use_client_side_checkout=True, extra_parameters=extra_parameters ) # This parameter is only used by the Web/Mobile flow. It is not needed for for Silent Order POST. parameters.pop('payment_page_url', None) # Ensure that the response can be properly rendered so that we # don't have to deal with thawing the basket in the event of an error. response = JsonResponse({'form_fields': parameters}) # Freeze the basket since the user is paying for it now. basket.freeze() return response
def form_valid(self, form): data = form.cleaned_data basket = data['basket'] request = self.request user = request.user # Add extra parameters for Silent Order POST extra_parameters = { 'payment_method': 'card', 'unsigned_field_names': ','.join(Cybersource.PCI_FIELDS), 'bill_to_email': user.email, 'device_fingerprint_id': request.session.session_key, } for source, destination in six.iteritems(self.FIELD_MAPPINGS): extra_parameters[destination] = clean_field_value(data[source]) parameters = Cybersource(self.request.site).get_transaction_parameters( basket, use_client_side_checkout=True, extra_parameters=extra_parameters) logger.info( 'Parameters signed for CyberSource transaction [%s], associated with basket [%d].', parameters.get('transaction_id'), basket.id) # This parameter is only used by the Web/Mobile flow. It is not needed for for Silent Order POST. parameters.pop('payment_page_url', None) # Ensure that the response can be properly rendered so that we # don't have to deal with thawing the basket in the event of an error. response = JsonResponse({'form_fields': parameters}) # Freeze the basket since the user is paying for it now. basket.freeze() return response
def request_apple_pay_authorization(self, basket, billing_address, payment_token): """ Authorizes an Apple Pay payment. For details on the process, see the CyberSource Simple Order API documentation at https://www.cybersource.com/developers/integration_methods/apple_pay/. Args: basket (Basket) billing_address (BillingAddress) payment_token (dict) Returns: HandledProcessorResponse Raises: GatewayError """ try: client = Client(self.soap_api_url, wsse=UsernameToken(self.merchant_id, self.transaction_key)) card_type = APPLE_PAY_CYBERSOURCE_CARD_TYPE_MAP[ payment_token['paymentMethod']['network'].lower()] bill_to = { 'firstName': billing_address.first_name, 'lastName': billing_address.last_name, 'street1': billing_address.line1, 'street2': billing_address.line2, 'city': billing_address.line4, 'state': billing_address.state, 'postalCode': billing_address.postcode, 'country': billing_address.country.iso_3166_1_a2, 'email': basket.owner.email, } purchase_totals = { 'currency': basket.currency, 'grandTotalAmount': str(basket.total_incl_tax), } encrypted_payment = { 'descriptor': 'RklEPUNPTU1PTi5BUFBMRS5JTkFQUC5QQVlNRU5U', 'data': base64.b64encode(json.dumps(payment_token['paymentData'])), 'encoding': 'Base64', } card = { 'cardType': card_type, } auth_service = { 'run': 'true', } capture_service = { 'run': 'true', } # Enable Export Compliance for SDN validation, amongst other checks. # See https://www.cybersource.com/products/fraud_management/export_compliance/ export_service = { 'run': 'true', } item = [{ 'id': index, 'productCode': line.product.get_product_class().slug, 'productName': clean_field_value(line.product.title), 'quantity': line.quantity, 'productSKU': line.stockrecord.partner_sku, 'taxAmount': str(line.line_tax), 'unitPrice': str(line.unit_price_incl_tax), } for index, line in enumerate(basket.all_lines())] response = client.service.runTransaction( merchantID=self.merchant_id, merchantReferenceCode=basket.order_number, billTo=bill_to, purchaseTotals=purchase_totals, encryptedPayment=encrypted_payment, card=card, ccAuthService=auth_service, ccCaptureService=capture_service, exportService=export_service, paymentSolution='001', item=item, ) except: msg = 'An error occurred while authorizing an Apple Pay (via CyberSource) for basket [{}]'.format( basket.id) logger.exception(msg) raise GatewayError(msg) request_id = response.requestID ppr = self.record_processor_response(serialize_object(response), transaction_id=request_id, basket=basket) if response.decision == 'ACCEPT': currency = basket.currency total = basket.total_incl_tax transaction_id = request_id return HandledProcessorResponse( transaction_id=transaction_id, total=total, currency=currency, card_number='Apple Pay', card_type=CYBERSOURCE_CARD_TYPE_MAP.get(card_type)) else: msg = ( 'CyberSource rejected an Apple Pay authorization request for basket [{basket_id}]. ' 'Complete response has been recorded in entry [{response_id}]') msg = msg.format(basket_id=basket.id, response_id=ppr.id) logger.warning(msg) raise GatewayError(msg)
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'))), 'override_custom_cancel_page': self.cancel_page_url, } # 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' 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) # 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 = 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!' ) return parameters
def test_clean_field_value(self): """ Verify the passed value is cleaned of specific special characters. """ value = 'Some^text:\'test-value' self.assertEqual(clean_field_value(value), 'Sometexttest-value')
def form_valid(self, form): data = form.cleaned_data basket = data['basket'] request = self.request user = request.user hit_count = checkSDN(request, data['first_name'] + ' ' + data['last_name'], data['city'], data['country']) if hit_count > 0: logger.info( 'SDNCheck function called for basket [%d]. It received %d hit(s).', request.basket.id, hit_count, ) response_to_return = { 'error': 'There was an error submitting the basket', 'sdn_check_failure': { 'hit_count': hit_count } } return JsonResponse(response_to_return, status=403) logger.info( 'SDNCheck function called for basket [%d]. It did not receive a hit.', request.basket.id, ) # Add extra parameters for Silent Order POST extra_parameters = { 'payment_method': 'card', 'unsigned_field_names': ','.join(Cybersource.PCI_FIELDS), 'bill_to_email': user.email, # Fall back to order number when there is no session key (JWT auth) 'device_fingerprint_id': request.session.session_key or basket.order_number, } for source, destination in six.iteritems(self.FIELD_MAPPINGS): extra_parameters[destination] = clean_field_value(data[source]) parameters = Cybersource(self.request.site).get_transaction_parameters( basket, use_client_side_checkout=True, extra_parameters=extra_parameters) logger.info( 'Parameters signed for CyberSource transaction [%s], associated with basket [%d].', # TODO: transaction_id is None in logs. This should be fixed. parameters.get('transaction_id'), basket.id) # This parameter is only used by the Web/Mobile flow. It is not needed for for Silent Order POST. parameters.pop('payment_page_url', None) # Ensure that the response can be properly rendered so that we # don't have to deal with thawing the basket in the event of an error. response = JsonResponse({'form_fields': parameters}) basket_add_organization_attribute(basket, data) # Freeze the basket since the user is paying for it now. basket.freeze() return 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__) 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 _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__) # 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
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') ) ), 'override_custom_cancel_page': self.cancel_page_url, } # 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.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' for index, line in enumerate(basket.lines.all()): 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) # 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 = 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!') return parameters
def test_clean_field_value(self): """ Verify the passed value is cleaned of specific special characters. """ value = 'Some^text:\'test-value' self.assertEqual(clean_field_value(value), 'Sometexttest-value')