<commit_before>""" CyberSource payment processing. """


import base64
import datetime
import json
import logging
import uuid
from decimal import Decimal

from CyberSource import (
    GeneratePublicKeyRequest, KeyGenerationApi, Ptsv2paymentsClientReferenceInformation, Ptsv2paymentsProcessingInformation, Ptsv2paymentsTokenInformation,
    Ptsv2paymentsOrderInformationAmountDetails, Ptsv2paymentsOrderInformationBillTo, Ptsv2paymentsOrderInformationLineItems,
    Ptsv2paymentsOrderInformationInvoiceDetails, Ptsv2paymentsOrderInformation, PaymentsApi, Ptsv2paymentsMerchantDefinedInformation,
    CreatePaymentRequest
)
from CyberSource.rest import ApiException
from django.conf import settings
from django.urls import reverse
import jwt
from jwt.algorithms import RSAAlgorithm
from oscar.apps.payment.exceptions import GatewayError, TransactionDeclined, UserCancelled
from oscar.core.loading import get_class, get_model
from zeep import Client
from zeep.helpers import serialize_object
from zeep.wsse import UsernameToken

from ecommerce.core.constants import ISO_8601_FORMAT
from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.constants import APPLE_PAY_CYBERSOURCE_CARD_TYPE_MAP, CYBERSOURCE_CARD_TYPE_MAP
from ecommerce.extensions.payment.exceptions import (
    AuthorizationError,
    DuplicateReferenceNumber,
    ExcessivePaymentForOrderError,
    InvalidCybersourceDecision,
    InvalidSignatureError,
    PartialAuthorizationError,
    PCIViolation,
    ProcessorMisconfiguredError,
    RedundantPaymentNotificationError
)
from ecommerce.extensions.payment.helpers import sign
from ecommerce.extensions.payment.processors import (
    ApplePayMixin,
    BaseClientSidePaymentProcessor,
    HandledProcessorResponse
)
from ecommerce.extensions.payment.utils import clean_field_value, get_basket_program_uuid

logger = logging.getLogger(__name__)

Order = get_model('order', 'Order')
OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator')
PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse')



def del_none(d):
    for key, value in list(d.items()):
        if value is None:
            del d[key]
        elif isinstance(value, dict):
            del_none(value)
    return d


class Cybersource(ApplePayMixin, BaseClientSidePaymentProcessor):
    """
    CyberSource Secure Acceptance Web/Mobile (February 2015)

    For reference, see
    http://apps.cybersource.com/library/documentation/dev_guides/Secure_Acceptance_WM/Secure_Acceptance_WM.pdf.
    """

    NAME = 'cybersource'
    PCI_FIELDS = ('card_cvn', 'card_expiry_date', 'card_number', 'card_type',)

    def __init__(self, site):
        """
        Constructs a new instance of the CyberSource processor.

        Raises:
            KeyError: If no settings configured for this payment processor
            AttributeError: If LANGUAGE_CODE setting is not set.
        """

        super(Cybersource, self).__init__(site)
        configuration = self.configuration
        self.soap_api_url = configuration['soap_api_url']
        self.merchant_id = configuration['merchant_id']
        self.transaction_key = configuration['transaction_key']
        self.send_level_2_3_details = configuration.get('send_level_2_3_details', True)
        self.language_code = settings.LANGUAGE_CODE

        # Secure Acceptance parameters
        # NOTE: Silent Order POST is the preferred method of checkout as it allows us to completely control
        # the checkout UX. Secure Acceptance, on the other hand, redirects the purchaser to a page controlled
        # by CyberSource.
        self.profile_id = configuration.get('profile_id')
        self.access_key = configuration.get('access_key')
        self.secret_key = configuration.get('secret_key')
        self.payment_page_url = configuration.get('payment_page_url')

        # Silent Order POST parameters
        self.sop_profile_id = configuration.get('sop_profile_id')
        self.sop_access_key = configuration.get('sop_access_key')
        self.sop_secret_key = configuration.get('sop_secret_key')
        self.sop_payment_page_url = configuration.get('sop_payment_page_url')

        sa_configured = all((self.access_key, self.payment_page_url, self.profile_id, self.secret_key))
        sop_configured = all([self.sop_access_key, self.sop_payment_page_url, self.sop_profile_id, self.sop_secret_key])
        assert sop_configured or sa_configured, \
            'CyberSource processor must be configured for Silent Order POST and/or Secure Acceptance'

        # Apple Pay configuration
        self.apple_pay_enabled = self.site.siteconfiguration.enable_apple_pay
        self.apple_pay_merchant_identifier = configuration.get('apple_pay_merchant_identifier', '')
        self.apple_pay_merchant_id_certificate_path = configuration.get('apple_pay_merchant_id_certificate_path', '')
        self.apple_pay_country_code = configuration.get('apple_pay_country_code', '')

        # Flex Microform configuration
        self.flex_run_environment = configuration.get('flex_run_environment', 'cybersource.environment.SANDBOX')
        self.flex_shared_secret_key_id = configuration.get('flex_shared_secret_key_id')
        self.flex_shared_secret_key = configuration.get('flex_shared_secret_key')
        self.flex_target_origin = self.site.siteconfiguration.payment_microfrontend_url
        self.cybersource_api_config = {
            'authentication_type': 'http_signature',
            'run_environment': self.flex_run_environment,
            'merchantid': self.merchant_id,
            'merchant_keyid': self.flex_shared_secret_key_id,
            'merchant_secretkey': self.flex_shared_secret_key,
        }

    @property
    def cancel_page_url(self):
        return get_ecommerce_url(self.configuration['cancel_checkout_path'])

    @property
    def client_side_payment_url(self):
        return self.sop_payment_page_url
    
    def get_capture_context(self):
        # To delete None values in Input Request Json body

        requestObj = GeneratePublicKeyRequest(
            encryption_type='RsaOaep256',
            target_origin=self.flex_target_origin,
        )
        requestObj = del_none(requestObj.__dict__)
        requestObj = json.dumps(requestObj)

        api_instance = KeyGenerationApi(self.cybersource_api_config)
        return_data, status, body = api_instance.generate_public_key(requestObj, format='JWT')

        return {'key_id': return_data.key_id}

    def get_transaction_parameters(self, basket, request=None, use_client_side_checkout=False, **kwargs):
        """
        Generate a dictionary of signed parameters CyberSource requires to complete a transaction.

        Arguments:
            use_client_side_checkout:
            basket (Basket): The basket of products being purchased.
            request (Request, optional): A Request object which could be used to construct an absolute URL; not
                used by this method.
            use_client_side_checkout (bool, optional): Indicates if the Silent Order POST profile should be used.
            **kwargs: Additional parameters.

        Keyword Arguments:
            extra_parameters (dict): Additional signed parameters that should be included in the signature
                and returned dict. Note that these parameters will override any default values.

        Returns:
            dict: CyberSource-specific parameters required to complete a transaction, including a signature.
        """
        sop_config_values = (self.sop_access_key, self.sop_payment_page_url, self.sop_profile_id, self.sop_secret_key,)
        if use_client_side_checkout and not all(sop_config_values):
            raise ProcessorMisconfiguredError(
                'CyberSource Silent Order POST cannot be used unless a profile ID, access key, '
                'secret key, and payment page URL are ALL configured in settings.'
            )

        parameters = self._generate_parameters(basket, use_client_side_checkout, **kwargs)

        # Sign all fields
        parameters['signed_field_names'] = ','.join(sorted(parameters.keys()))
        parameters['signature'] = self._generate_signature(parameters, use_client_side_checkout)

        payment_page_url = self.sop_payment_page_url if use_client_side_checkout else self.payment_page_url
        parameters['payment_page_url'] = payment_page_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 handle_processor_response(self, response, basket=None):
        """
        Handle a response (i.e., "merchant notification") from CyberSource.

        Arguments:
            response (dict): Dictionary of parameters received from the payment processor.

        Keyword Arguments:
            basket (Basket): Basket being purchased via the payment processor.

        Raises:
            AuthorizationError: Authorization was declined.
            UserCancelled: Indicates the user cancelled payment.
            TransactionDeclined: Indicates the payment was declined by the processor.
            GatewayError: Indicates a general error on the part of the processor.
            InvalidCyberSourceDecision: Indicates an unknown decision value.
                Known values are ACCEPT, CANCEL, DECLINE, ERROR, REVIEW.
            PartialAuthorizationError: Indicates only a portion of the requested amount was authorized.

        Returns:
            HandledProcessorResponse
        """

        # Validate the signature
        if not self.is_signature_valid(response):
            raise InvalidSignatureError

        # Raise an exception for payments that were not accepted. Consuming code should be responsible for handling
        # and logging the exception.
        decision = response['decision'].lower()
        if decision != 'accept':
            reason_code = int(response['reason_code'])

            if decision == 'error' and reason_code == 104:
                # This means user submitted payment request twice within 15 min.
                # We need to check if user first payment notification was handled successfuly and user has an order
                # if user has an order we can raise DuplicateReferenceNumber exception else we need to continue
                # the order creation process. to upgrade user in correct course mode.
                if Order.objects.filter(number=response['req_reference_number']).exists():
                    raise DuplicateReferenceNumber
                logger.info(
                    'Received duplicate CyberSource payment notification for basket [%d] which is not associated '
                    'with any existing order. Continuing to validation and order creation processes.',
                    basket.id,
                )
            else:
                raise {
                    'cancel': UserCancelled,
                    'decline': TransactionDeclined,
                    'error': GatewayError,
                    'review': AuthorizationError,
                }.get(decision, InvalidCybersourceDecision)

        transaction_id = response.get('transaction_id', '')  # Error Notifications do not include a transaction id.
        if transaction_id and decision == 'accept':
            if Order.objects.filter(number=response['req_reference_number']).exists():
                if PaymentProcessorResponse.objects.filter(transaction_id=transaction_id).exists():
                    raise RedundantPaymentNotificationError
                raise ExcessivePaymentForOrderError
<commit_msg>Add an Enum to capture known decision values<commit_after>""" CyberSource payment processing. """


import base64
import datetime
from enum import Enum
import json
import logging
import uuid
from decimal import Decimal

from CyberSource import (
    GeneratePublicKeyRequest, KeyGenerationApi, Ptsv2paymentsClientReferenceInformation, Ptsv2paymentsProcessingInformation, Ptsv2paymentsTokenInformation,
    Ptsv2paymentsOrderInformationAmountDetails, Ptsv2paymentsOrderInformationBillTo, Ptsv2paymentsOrderInformationLineItems,
    Ptsv2paymentsOrderInformationInvoiceDetails, Ptsv2paymentsOrderInformation, PaymentsApi, Ptsv2paymentsMerchantDefinedInformation,
    CreatePaymentRequest
)
from CyberSource.rest import ApiException
from django.conf import settings
from django.urls import reverse
import jwt
from jwt.algorithms import RSAAlgorithm
from oscar.apps.payment.exceptions import GatewayError, TransactionDeclined, UserCancelled
from oscar.core.loading import get_class, get_model
from zeep import Client
from zeep.helpers import serialize_object
from zeep.wsse import UsernameToken

from ecommerce.core.constants import ISO_8601_FORMAT
from ecommerce.core.url_utils import get_ecommerce_url
from ecommerce.extensions.checkout.utils import get_receipt_page_url
from ecommerce.extensions.payment.constants import APPLE_PAY_CYBERSOURCE_CARD_TYPE_MAP, CYBERSOURCE_CARD_TYPE_MAP
from ecommerce.extensions.payment.exceptions import (
    AuthorizationError,
    DuplicateReferenceNumber,
    ExcessivePaymentForOrderError,
    InvalidCybersourceDecision,
    InvalidSignatureError,
    PartialAuthorizationError,
    PCIViolation,
    ProcessorMisconfiguredError,
    RedundantPaymentNotificationError
)
from ecommerce.extensions.payment.helpers import sign
from ecommerce.extensions.payment.processors import (
    ApplePayMixin,
    BaseClientSidePaymentProcessor,
    HandledProcessorResponse
)
from ecommerce.extensions.payment.utils import clean_field_value, get_basket_program_uuid

logger = logging.getLogger(__name__)

Order = get_model('order', 'Order')
OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator')
PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse')



def del_none(d):
    for key, value in list(d.items()):
        if value is None:
            del d[key]
        elif isinstance(value, dict):
            del_none(value)
    return d


class Decision(Enum):
    accept = 'accept'
    cancel = 'cancel'
    decline = 'decline'
    error = 'error'
    review = 'review'
    invalid = 'invalid'  # Used when the Cybersource decision doesn't match a known decision type


class Cybersource(ApplePayMixin, BaseClientSidePaymentProcessor):
    """
    CyberSource Secure Acceptance Web/Mobile (February 2015)

    For reference, see
    http://apps.cybersource.com/library/documentation/dev_guides/Secure_Acceptance_WM/Secure_Acceptance_WM.pdf.
    """

    NAME = 'cybersource'
    PCI_FIELDS = ('card_cvn', 'card_expiry_date', 'card_number', 'card_type',)

    def __init__(self, site):
        """
        Constructs a new instance of the CyberSource processor.

        Raises:
            KeyError: If no settings configured for this payment processor
            AttributeError: If LANGUAGE_CODE setting is not set.
        """

        super(Cybersource, self).__init__(site)
        configuration = self.configuration
        self.soap_api_url = configuration['soap_api_url']
        self.merchant_id = configuration['merchant_id']
        self.transaction_key = configuration['transaction_key']
        self.send_level_2_3_details = configuration.get('send_level_2_3_details', True)
        self.language_code = settings.LANGUAGE_CODE

        # Secure Acceptance parameters
        # NOTE: Silent Order POST is the preferred method of checkout as it allows us to completely control
        # the checkout UX. Secure Acceptance, on the other hand, redirects the purchaser to a page controlled
        # by CyberSource.
        self.profile_id = configuration.get('profile_id')
        self.access_key = configuration.get('access_key')
        self.secret_key = configuration.get('secret_key')
        self.payment_page_url = configuration.get('payment_page_url')

        # Silent Order POST parameters
        self.sop_profile_id = configuration.get('sop_profile_id')
        self.sop_access_key = configuration.get('sop_access_key')
        self.sop_secret_key = configuration.get('sop_secret_key')
        self.sop_payment_page_url = configuration.get('sop_payment_page_url')

        sa_configured = all((self.access_key, self.payment_page_url, self.profile_id, self.secret_key))
        sop_configured = all([self.sop_access_key, self.sop_payment_page_url, self.sop_profile_id, self.sop_secret_key])
        assert sop_configured or sa_configured, \
            'CyberSource processor must be configured for Silent Order POST and/or Secure Acceptance'

        # Apple Pay configuration
        self.apple_pay_enabled = self.site.siteconfiguration.enable_apple_pay
        self.apple_pay_merchant_identifier = configuration.get('apple_pay_merchant_identifier', '')
        self.apple_pay_merchant_id_certificate_path = configuration.get('apple_pay_merchant_id_certificate_path', '')
        self.apple_pay_country_code = configuration.get('apple_pay_country_code', '')

        # Flex Microform configuration
        self.flex_run_environment = configuration.get('flex_run_environment', 'cybersource.environment.SANDBOX')
        self.flex_shared_secret_key_id = configuration.get('flex_shared_secret_key_id')
        self.flex_shared_secret_key = configuration.get('flex_shared_secret_key')
        self.flex_target_origin = self.site.siteconfiguration.payment_microfrontend_url
        self.cybersource_api_config = {
            'authentication_type': 'http_signature',
            'run_environment': self.flex_run_environment,
            'merchantid': self.merchant_id,
            'merchant_keyid': self.flex_shared_secret_key_id,
            'merchant_secretkey': self.flex_shared_secret_key,
        }

    @property
    def cancel_page_url(self):
        return get_ecommerce_url(self.configuration['cancel_checkout_path'])

    @property
    def client_side_payment_url(self):
        return self.sop_payment_page_url
    
    def get_capture_context(self):
        # To delete None values in Input Request Json body

        requestObj = GeneratePublicKeyRequest(
            encryption_type='RsaOaep256',
            target_origin=self.flex_target_origin,
        )
        requestObj = del_none(requestObj.__dict__)
        requestObj = json.dumps(requestObj)

        api_instance = KeyGenerationApi(self.cybersource_api_config)
        return_data, status, body = api_instance.generate_public_key(requestObj, format='JWT')

        return {'key_id': return_data.key_id}

    def get_transaction_parameters(self, basket, request=None, use_client_side_checkout=False, **kwargs):
        """
        Generate a dictionary of signed parameters CyberSource requires to complete a transaction.

        Arguments:
            use_client_side_checkout:
            basket (Basket): The basket of products being purchased.
            request (Request, optional): A Request object which could be used to construct an absolute URL; not
                used by this method.
            use_client_side_checkout (bool, optional): Indicates if the Silent Order POST profile should be used.
            **kwargs: Additional parameters.

        Keyword Arguments:
            extra_parameters (dict): Additional signed parameters that should be included in the signature
                and returned dict. Note that these parameters will override any default values.

        Returns:
            dict: CyberSource-specific parameters required to complete a transaction, including a signature.
        """
        sop_config_values = (self.sop_access_key, self.sop_payment_page_url, self.sop_profile_id, self.sop_secret_key,)
        if use_client_side_checkout and not all(sop_config_values):
            raise ProcessorMisconfiguredError(
                'CyberSource Silent Order POST cannot be used unless a profile ID, access key, '
                'secret key, and payment page URL are ALL configured in settings.'
            )

        parameters = self._generate_parameters(basket, use_client_side_checkout, **kwargs)

        # Sign all fields
        parameters['signed_field_names'] = ','.join(sorted(parameters.keys()))
        parameters['signature'] = self._generate_signature(parameters, use_client_side_checkout)

        payment_page_url = self.sop_payment_page_url if use_client_side_checkout else self.payment_page_url
        parameters['payment_page_url'] = payment_page_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 handle_processor_response(self, response, basket=None):
        """
        Handle a response (i.e., "merchant notification") from CyberSource.

        Arguments:
            response (dict): Dictionary of parameters received from the payment processor.

        Keyword Arguments:
            basket (Basket): Basket being purchased via the payment processor.

        Raises:
            AuthorizationError: Authorization was declined.
            UserCancelled: Indicates the user cancelled payment.
            TransactionDeclined: Indicates the payment was declined by the processor.
            GatewayError: Indicates a general error on the part of the processor.
            InvalidCyberSourceDecision: Indicates an unknown decision value.
                Known values are ACCEPT, CANCEL, DECLINE, ERROR, REVIEW.
            PartialAuthorizationError: Indicates only a portion of the requested amount was authorized.

        Returns:
            HandledProcessorResponse
        """

        # Validate the signature
        if not self.is_signature_valid(response):
            raise InvalidSignatureError

        # Raise an exception for payments that were not accepted. Consuming code should be responsible for handling
        # and logging the exception.
        try:
            decision = Decision(response['decision'].lower())
        except ValueError:
            decision = Decision.invalid

        if decision != Decision.accept:
            reason_code = int(response['reason_code'])

            if decision == Decision.error and reason_code == 104:
                # This means user submitted payment request twice within 15 min.
                # We need to check if user first payment notification was handled successfuly and user has an order
                # if user has an order we can raise DuplicateReferenceNumber exception else we need to continue
                # the order creation process. to upgrade user in correct course mode.
                if Order.objects.filter(number=response['req_reference_number']).exists():
                    raise DuplicateReferenceNumber
                logger.info(
                    'Received duplicate CyberSource payment notification for basket [%d] which is not associated '
                    'with any existing order. Continuing to validation and order creation processes.',
                    basket.id,
                )
            else:
                raise {
                    Decision.cancel: UserCancelled,
                    Decision.decline: TransactionDeclined,
                    Decision.error: GatewayError,
                    Decision.review: AuthorizationError,
                }.get(decision, InvalidCybersourceDecision)

        transaction_id = response.get('transaction_id', '')  # Error Notifications do not include a transaction id.
        if transaction_id and decision == Decision.accept:
            if Order.objects.filter(number=response['req_reference_number']).exists():
                if PaymentProcessorResponse.objects.filter(transaction_id=transaction_id).exists():
                    raise RedundantPaymentNotificationError
                raise ExcessivePaymentForOrderError

{'fraction_non_alphanumeric': 0.07284518949593213, 'fraction_numerical': 0.0026636555777526856, 'mean_word_length': 2.8267228007976954, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7055180', 'n_tokens_mistral': 9138, 'n_tokens_neox': 8798, 'n_words': 2295}