def handle_payment(self, order_number, total, **kwargs):
        """
        Complete payment with PayPal - this calls the 'DoExpressCheckout'
        method to capture the money from the initial transaction.
        """
        try:
            if int(kwargs['txn'].value('BILLINGAGREEMENTACCEPTEDSTATUS')) == 1:
                utc_today = timezone.now().utcnow().date()
                start_date = utc_today.strftime('%Y-%m-%dT%H:%M:%SZ') 
                desc = ''
                billing_period = None
                billing_frequency = None
                basket = Basket.objects.filter(owner=self.request.user, status=Basket.FROZEN).order_by('-date_created')[0]
                for index, line in enumerate(basket.all_lines()):
                    product = line.product
                    recurring_profile = get_recurring_profile(product)
                    if recurring_profile is not None:
                        billing_period = recurring_profile.get('billing_period')
                        billing_frequency = recurring_profile.get('billing_frequency')
                        desc = recurring_profile.get('billing_description')
                        total_periods = recurring_profile.get('total_periods')
                
                confirm_txn = create_recurring_payment(
                    kwargs['payer_id'],
                    kwargs['token'],
                    kwargs['txn'].amount,
                    kwargs['txn'].currency,
                    start_date,
                    desc,
                    billing_period,
                    billing_frequency,
                    total_periods
                )
            else:
                confirm_txn = confirm_transaction(
                    kwargs['payer_id'], kwargs['token'], kwargs['txn'].amount,
                    kwargs['txn'].currency)
        except PayPalError:
            raise UnableToTakePayment()
        if not confirm_txn.is_successful:
            raise UnableToTakePayment()

        # Record payment source and event
        source_type, is_created = SourceType.objects.get_or_create(
            name='PayPal')
        source = Source(source_type=source_type,
                        currency=confirm_txn.currency,
                        amount_allocated=confirm_txn.amount,
                        amount_debited=confirm_txn.amount)
        self.add_payment_source(source)
        self.add_payment_event('Settled', confirm_txn.amount,
                               reference=confirm_txn.correlation_id)
Exemple #2
0
    def handle_payment(self, order_number, total, **kwargs):
        """
        Complete payment with PayPal - this calls the 'DoExpressCheckout'
        method to capture the money from the initial transaction.
        """
        try:
            if int(kwargs['txn'].value('BILLINGAGREEMENTACCEPTEDSTATUS')) == 1:
                utc_today = timezone.now().utcnow().date()
                start_date = utc_today.strftime('%Y-%m-%dT%H:%M:%SZ')
                desc = ''
                billing_period = None
                billing_frequency = None
                basket = Basket.objects.filter(
                    owner=self.request.user,
                    status=Basket.FROZEN).order_by('-date_created')[0]
                for index, line in enumerate(basket.all_lines()):
                    product = line.product
                    recurring_profile = get_recurring_profile(product)
                    if recurring_profile is not None:
                        billing_period = recurring_profile.get(
                            'billing_period')
                        billing_frequency = recurring_profile.get(
                            'billing_frequency')
                        desc = recurring_profile.get('billing_description')
                        total_periods = recurring_profile.get('total_periods')

                confirm_txn = create_recurring_payment(
                    kwargs['payer_id'], kwargs['token'], kwargs['txn'].amount,
                    kwargs['txn'].currency, start_date, desc, billing_period,
                    billing_frequency, total_periods)
            else:
                confirm_txn = confirm_transaction(kwargs['payer_id'],
                                                  kwargs['token'],
                                                  kwargs['txn'].amount,
                                                  kwargs['txn'].currency)
        except PayPalError:
            raise UnableToTakePayment()
        if not confirm_txn.is_successful:
            raise UnableToTakePayment()

        # Record payment source and event
        source_type, is_created = SourceType.objects.get_or_create(
            name='PayPal')
        source = Source(source_type=source_type,
                        currency=confirm_txn.currency,
                        amount_allocated=confirm_txn.amount,
                        amount_debited=confirm_txn.amount)
        self.add_payment_source(source)
        self.add_payment_event('Settled',
                               confirm_txn.amount,
                               reference=confirm_txn.correlation_id)
def set_txn(basket, shipping_methods, currency, return_url, cancel_url, update_url=None,
            action=SALE, user=None, user_address=None, shipping_method=None,
            shipping_address=None, no_shipping=False, paypal_params=None):
    """
    Register the transaction with PayPal to get a token which we use in the
    redirect URL.  This is the 'SetExpressCheckout' from their documentation.

    There are quite a few options that can be passed to PayPal to configure
    this request - most are controlled by PAYPAL_* settings.
    """
    # Default parameters (taken from global settings).  These can be overridden
    # and customised using the paypal_params parameter.
    _params = {
        'CUSTOMERSERVICENUMBER': getattr(
            settings, 'PAYPAL_CUSTOMER_SERVICES_NUMBER', None),
        'SOLUTIONTYPE': getattr(settings, 'PAYPAL_SOLUTION_TYPE', None),
        'LANDINGPAGE': getattr(settings, 'PAYPAL_LANDING_PAGE', None),
        'BRANDNAME': getattr(settings, 'PAYPAL_BRAND_NAME', None),

        # Display settings
        'PAGESTYLE': getattr(settings, 'PAYPAL_PAGESTYLE', None),
        'HDRIMG': getattr(settings, 'PAYPAL_HEADER_IMG', None),
        'PAYFLOWCOLOR': getattr(settings, 'PAYPAL_PAYFLOW_COLOR', None),

        # Think these settings maybe deprecated in latest version of PayPal's
        # API
        'HDRBACKCOLOR': getattr(settings, 'PAYPAL_HEADER_BACK_COLOR', None),
        'HDRBORDERCOLOR': getattr(
            settings, 'PAYPAL_HEADER_BORDER_COLOR', None),

        'LOCALECODE': getattr(settings, 'PAYPAL_LOCALE', None),

        'ALLOWNOTE': getattr(settings, 'PAYPAL_ALLOW_NOTE', True),
        'CALLBACKTIMEOUT': getattr(settings, 'PAYPAL_CALLBACK_TIMEOUT', 3)
    }
    confirm_shipping_addr = getattr(settings, 'PAYPAL_CONFIRM_SHIPPING', None)
    if confirm_shipping_addr and not no_shipping:
        _params['REQCONFIRMSHIPPING'] = 1
    if paypal_params:
        _params.update(paypal_params)

    locale = _params.get('LOCALECODE', None)
    if locale:
        valid_choices = ('AU', 'DE', 'FR', 'GB', 'IT', 'ES', 'JP', 'US')
        if locale not in valid_choices:
            raise ImproperlyConfigured(
                "'%s' is not a valid locale code" % locale)

    # Boolean values become integers
    _params.update((k, int(v)) for k, v in _params.items() if isinstance(v, bool))

    # Remove None values
    params = dict((k, v) for k, v in _params.items() if v is not None)

    # PayPal have an upper limit on transactions.  It's in dollars which is a
    # fiddly to work with.  Lazy solution - only check when dollars are used as
    # the PayPal currency.
    amount = basket.total_incl_tax
    if currency == 'USD' and amount > 10000:
        msg = 'PayPal can only be used for orders up to 10000 USD'
        logger.error(msg)
        raise express_exceptions.InvalidBasket(_(msg))

    if amount <= 0:
        msg = 'The basket total is zero so no payment is required'
        logger.error(msg)
        raise express_exceptions.InvalidBasket(_(msg))

    # PAYMENTREQUEST_0_AMT should include tax, shipping and handling
    params.update({
        'PAYMENTREQUEST_0_AMT': amount,
        'PAYMENTREQUEST_0_CURRENCYCODE': currency,
        'RETURNURL': return_url,
        'CANCELURL': cancel_url,
        'PAYMENTREQUEST_0_PAYMENTACTION': action,
    })

    # Add item details
    index = 0
    is_recurring = False
    for index, line in enumerate(basket.all_lines()):
    
    
        product = line.product
        recurring_profile = get_recurring_profile(product)
        if recurring_profile is not None:
            is_recurring = True
            
            if is_recurring and basket.num_items > 1:
                raise exceptions.PayPalError('Only a single recurring payment is supported')
                
            params['L_BILLINGTYPE0'] = 'RecurringPayments'
            params['L_BILLINGAGREEMENTDESCRIPTION0'] = recurring_profile.get(
                'billing_description'
            )
            
        else:
            
            params['L_PAYMENTREQUEST_0_NAME%d' % index] = product.get_title()
            params['L_PAYMENTREQUEST_0_NUMBER%d' % index] = (product.upc if
                                                            product.upc else '')
            desc = ''
            if product.description:
                desc = _format_description(product.description)
            params['L_PAYMENTREQUEST_0_DESC%d' % index] = desc
            # Note, we don't include discounts here - they are handled as separate
            # lines - see below
            params['L_PAYMENTREQUEST_0_AMT%d' % index] = _format_currency(
                line.unit_price_incl_tax)
            params['L_PAYMENTREQUEST_0_QTY%d' % index] = line.quantity

    if not is_recurring:
        # If the order has discounts associated with it, the way PayPal suggests
        # using the API is to add a separate item for the discount with the value
        # as a negative price.  See "Integrating Order Details into the Express
        # Checkout Flow"
        # https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_api_ECCustomizing

        # Iterate over the 3 types of discount that can occur
        for discount in basket.offer_discounts:
            index += 1
            name = _("Special Offer: %s") % discount['name']
            params['L_PAYMENTREQUEST_0_NAME%d' % index] = name
            params['L_PAYMENTREQUEST_0_DESC%d' % index] = _format_description(name)
            params['L_PAYMENTREQUEST_0_AMT%d' % index] = _format_currency(
                -discount['discount'])
            params['L_PAYMENTREQUEST_0_QTY%d' % index] = 1
        for discount in basket.voucher_discounts:
            index += 1
            name = "%s (%s)" % (discount['voucher'].name,
                                discount['voucher'].code)
            params['L_PAYMENTREQUEST_0_NAME%d' % index] = name
            params['L_PAYMENTREQUEST_0_DESC%d' % index] = _format_description(name)
            params['L_PAYMENTREQUEST_0_AMT%d' % index] = _format_currency(
                -discount['discount'])
            params['L_PAYMENTREQUEST_0_QTY%d' % index] = 1
        for discount in basket.shipping_discounts:
            index += 1
            name = _("Shipping Offer: %s") % discount['name']
            params['L_PAYMENTREQUEST_0_NAME%d' % index] = name
            params['L_PAYMENTREQUEST_0_DESC%d' % index] = _format_description(name)
            params['L_PAYMENTREQUEST_0_AMT%d' % index] = _format_currency(
                -discount['discount'])
            params['L_PAYMENTREQUEST_0_QTY%d' % index] = 1

        # We include tax in the prices rather than separately as that's how it's
        # done on most British/Australian sites.  Will need to refactor in the
        # future no doubt.

        # Note that the following constraint must be met
        #
        # PAYMENTREQUEST_0_AMT = (
        #     PAYMENTREQUEST_0_ITEMAMT +
        #     PAYMENTREQUEST_0_TAXAMT +
        #     PAYMENTREQUEST_0_SHIPPINGAMT +
        #     PAYMENTREQUEST_0_HANDLINGAMT)
        #
        # Hence, if tax is to be shown then it has to be aggregated up to the order
        # level.
        params['PAYMENTREQUEST_0_ITEMAMT'] = _format_currency(
            basket.total_incl_tax)
        params['PAYMENTREQUEST_0_TAXAMT'] = _format_currency(D('0.00'))

    # Instant update callback information
    if update_url:
        params['CALLBACK'] = update_url

        # Contact details and address details - we provide these as it would make
        # the PayPal registration process smoother is the user doesn't already have
        # an account.
        if user:
            params['EMAIL'] = user.email
        if user_address:
            params['SHIPTONAME'] = user_address.name
            params['SHIPTOSTREET'] = user_address.line1
            params['SHIPTOSTREET2'] = user_address.line2
            params['SHIPTOCITY'] = user_address.line4
            params['SHIPTOSTATE'] = user_address.state
            params['SHIPTOZIP'] = user_address.postcode
            params['SHIPTOCOUNTRYCODE'] = user_address.country.iso_3166_1_a2

        # Shipping details (if already set) - we override the SHIPTO* fields and
        # set a flag to indicate that these can't be altered on the PayPal side.
        if shipping_method and shipping_address:
            params['ADDROVERRIDE'] = 1
            # It's recommend not to set 'confirmed shipping' if supplying the
            # shipping address directly.
            params['REQCONFIRMSHIPPING'] = 0
            params['SHIPTONAME'] = shipping_address.name
            params['SHIPTOSTREET'] = shipping_address.line1
            params['SHIPTOSTREET2'] = shipping_address.line2
            params['SHIPTOCITY'] = shipping_address.line4
            params['SHIPTOSTATE'] = shipping_address.state
            params['SHIPTOZIP'] = shipping_address.postcode
            params['SHIPTOCOUNTRYCODE'] = shipping_address.country.iso_3166_1_a2

            # For US addresses, we need to try and convert the state into 2 letter
            # code - otherwise we can get a 10736 error as the shipping address and
            # zipcode don't match the state. Very silly really.
            if params['SHIPTOCOUNTRYCODE'] == 'US':
                key = params['SHIPTOSTATE'].lower().strip()
                if key in us_states.STATES_NORMALIZED:
                    params['SHIPTOSTATE'] = us_states.STATES_NORMALIZED[key]

        elif no_shipping:
            params['NOSHIPPING'] = 1

        # Shipping charges
        params['PAYMENTREQUEST_0_SHIPPINGAMT'] = _format_currency(D('0.00'))
        max_charge = D('0.00')
        for index, method in enumerate(shipping_methods):
            is_default = index == 0
            params['L_SHIPPINGOPTIONISDEFAULT%d' % index] = 'true' if is_default else 'false'
            if hasattr(method, 'charge_incl_tax'):
                # Oscar < 0.8
                charge = method.charge_incl_tax
            else:
                cost = method.calculate(basket)
                charge = cost.incl_tax
            if charge > max_charge:
                max_charge = charge
            if is_default:
                params['PAYMENTREQUEST_0_SHIPPINGAMT'] = _format_currency(charge)
                params['PAYMENTREQUEST_0_AMT'] += charge
            params['L_SHIPPINGOPTIONNAME%d' % index] = six.text_type(method.name)
            params['L_SHIPPINGOPTIONAMOUNT%d' % index] = _format_currency(charge)

        # Set shipping charge explicitly if it has been passed
        if shipping_method:
            if hasattr(shipping_method, 'charge_incl_tax'):
                # Oscar < 0.8
                max_charge = charge = shipping_method.charge_incl_tax
            else:
                cost = shipping_method.calculate(basket)
                charge = cost.incl_tax
            params['PAYMENTREQUEST_0_SHIPPINGAMT'] = _format_currency(charge)
            params['PAYMENTREQUEST_0_AMT'] += charge

        # Both the old version (MAXAMT) and the new version (PAYMENT...) are needed
        # here - think it's a problem with the API.
        params['PAYMENTREQUEST_0_MAXAMT'] = _format_currency(amount + max_charge)
        params['MAXAMT'] = _format_currency(amount + max_charge)

        # Handling set to zero for now - I've never worked on a site that needed a
        # handling charge.
        params['PAYMENTREQUEST_0_HANDLINGAMT'] = _format_currency(D('0.00'))

        # Ensure that the total is formatted correctly.
        params['PAYMENTREQUEST_0_AMT'] = _format_currency(
            params['PAYMENTREQUEST_0_AMT'])

    txn = _fetch_response(SET_EXPRESS_CHECKOUT, params)

    # Construct return URL
    if getattr(settings, 'PAYPAL_SANDBOX_MODE', True):
        url = 'https://www.sandbox.paypal.com/webscr'
    else:
        url = 'https://www.paypal.com/webscr'
    params = (('cmd', '_express-checkout'),
              ('token', txn.token),)
    return '%s?%s' % (url, urlencode(params))