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