From 8ebfe1b20d9bf883263c7d895b9dc749f1de7a3c Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Fri, 2 Dec 2016 10:06:33 +0000 Subject: [PATCH 01/12] Fix to make shipping address from data passed from PayPal, per PayPal's specifcation --- paypal/express/views.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index 67f19608..e4bd4e0f 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -408,18 +408,18 @@ def post(self, request, *args, **kwargs): # Create a shipping address instance using the data passed back country_code = self.request.POST.get( - 'PAYMENTREQUEST_0_SHIPTOCOUNTRY', None) + 'SHIPTOCOUNTRY', None) try: country = Country.objects.get(iso_3166_1_a2=country_code) except Country.DoesNotExist: country = Country() shipping_address = ShippingAddress( - line1=self.request.POST.get('PAYMENTREQUEST_0_SHIPTOSTREET', ''), - line2=self.request.POST.get('PAYMENTREQUEST_0_SHIPTOSTREET2', ''), - line4=self.request.POST.get('PAYMENTREQUEST_0_SHIPTOCITY', ''), - state=self.request.POST.get('PAYMENTREQUEST_0_SHIPTOSTATE', ''), - postcode=self.request.POST.get('PAYMENTREQUEST_0_SHIPTOZIP', ''), + line1=self.request.POST.get('SHIPTOSTREET', ''), + line2=self.request.POST.get('SHIPTOSTREET2', ''), + line4=self.request.POST.get('SHIPTOCITY', ''), + state=self.request.POST.get('SHIPTOSTATE', ''), + postcode=self.request.POST.get('SHIPTOZIP', ''), country=country ) methods = Repository().get_shipping_methods( From 976e44ab4af16124f76cefa94bacf3c8dd3c9753 Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Fri, 2 Dec 2016 15:58:38 +0000 Subject: [PATCH 02/12] get_shipping_address() modified to set first name to '' when only a single name is returned --- paypal/express/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/paypal/express/views.py b/paypal/express/views.py index e4bd4e0f..261614a2 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -336,6 +336,7 @@ def get_shipping_address(self, basket): parts = ship_to_name.split() if len(parts) == 1: last_name = ship_to_name + first_name = '' elif len(parts) > 1: first_name = parts[0] last_name = " ".join(parts[1:]) From 1b4e3d1c90eaaf54da331bc64bfd5499c94ee109 Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Wed, 27 Mar 2019 17:20:55 +0000 Subject: [PATCH 03/12] Fixes to callbacks --- paypal/express/urls.py | 9 ++--- paypal/express/views.py | 81 ++++++++++++++++++++++++++++++----------- 2 files changed, 64 insertions(+), 26 deletions(-) diff --git a/paypal/express/urls.py b/paypal/express/urls.py index 1b97e521..2ffcdd20 100644 --- a/paypal/express/urls.py +++ b/paypal/express/urls.py @@ -1,10 +1,9 @@ -from django.conf.urls import * +from django.conf.urls import url from django.views.decorators.csrf import csrf_exempt from paypal.express import views - -urlpatterns = patterns('', +urlpatterns = [ # Views for normal flow that starts on the basket page url(r'^redirect/', views.RedirectView.as_view(), name='paypal-redirect'), url(r'^preview/(?P\d+)/$', @@ -15,10 +14,10 @@ url(r'^place-order/(?P\d+)/$', views.SuccessResponseView.as_view(), name='paypal-place-order'), # Callback for getting shipping options for a specific basket - url(r'^shipping-options/(?P\d+)/', + url(r'^shipping-options/(?P\d+)/(?P\w+)?', csrf_exempt(views.ShippingOptionsView.as_view()), name='paypal-shipping-options'), # View for using PayPal as a payment method url(r'^payment/', views.RedirectView.as_view(as_payment_method=True), name='paypal-direct-payment'), -) +] diff --git a/paypal/express/views.py b/paypal/express/views.py index 261614a2..1a6149eb 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -1,31 +1,29 @@ from __future__ import unicode_literals -from decimal import Decimal as D + import logging +from decimal import Decimal as D -from django.views.generic import RedirectView, View from django.conf import settings -from django.http import HttpResponse -from django.shortcuts import get_object_or_404 from django.contrib import messages from django.contrib.auth.models import AnonymousUser -from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect -from django.utils.http import urlencode +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404 +from django.urls import reverse from django.utils import six +from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ - -import oscar +from django.views.generic import RedirectView, View from oscar.apps.payment.exceptions import UnableToTakePayment +from oscar.apps.shipping.methods import FixedPrice, NoShippingRequired from oscar.core.exceptions import ModuleNotFoundError from oscar.core.loading import get_class, get_model -from oscar.apps.shipping.methods import FixedPrice, NoShippingRequired -from paypal.express.facade import ( - get_paypal_url, fetch_transaction_details, confirm_transaction) -from paypal.express.exceptions import ( - EmptyBasketException, MissingShippingAddressException, - MissingShippingMethodException, InvalidBasket) from paypal.exceptions import PayPalError +from paypal.express.exceptions import (EmptyBasketException, InvalidBasket, + MissingShippingAddressException, + MissingShippingMethodException) +from paypal.express.facade import (confirm_transaction, + fetch_transaction_details, get_paypal_url) # Load views dynamically PaymentDetailsView = get_class('checkout.views', 'PaymentDetailsView') @@ -64,8 +62,7 @@ def get_redirect_url(self, **kwargs): basket = self.build_submission()['basket'] url = self._get_redirect_url(basket, **kwargs) except PayPalError as ppe: - messages.error( - self.request, ppe.message) + messages.error(self.request, six.text_type(ppe)) if self.as_payment_method: url = reverse('checkout:payment-details') else: @@ -134,7 +131,7 @@ def _get_redirect_url(self, basket, **kwargs): # in testing mode params['host'] = self.request.META['HTTP_HOST'] - if user.is_authenticated(): + if user.is_authenticated: params['user'] = user params['paypal_params'] = self._get_paypal_params() @@ -232,7 +229,7 @@ def load_frozen_basket(self, basket_id): basket.strategy = Selector().strategy(self.request) # Re-apply any offers - Applicator().apply(request=self.request, basket=basket) + Applicator().apply(basket, self.request.user, request=self.request) return basket @@ -332,11 +329,10 @@ def get_shipping_address(self, basket): ship_to_name = self.txn.value('PAYMENTREQUEST_0_SHIPTONAME') if ship_to_name is None: return None - first_name = last_name = None + first_name = last_name = '' parts = ship_to_name.split() if len(parts) == 1: last_name = ship_to_name - first_name = '' elif len(parts) > 1: first_name = parts[0] last_name = " ".join(parts[1:]) @@ -392,6 +388,46 @@ def get_shipping_method(self, basket, shipping_address=None, **kwargs): class ShippingOptionsView(View): + def get(self, request, *args, **kwargs): + """ + We use the shipping address given to use by PayPal to + determine the available shipping method + """ + # Basket ID is passed within the URL path. We need to do this as some + # shipping options depend on the user and basket contents. PayPal do + # pass back details of the basket contents but it would be royal pain to + # reconstitute the basket based on those - easier to just to piggy-back + # the basket ID in the callback URL. + basket = get_object_or_404(Basket, id=kwargs['basket_id']) + user = basket.owner + if not user: + user = AnonymousUser() + + logger.info(kwargs['country_code']) + + # Create a shipping address instance using the data passed back + country_code = self.request.POST.get( + 'SHIPTOCOUNTRY', None) + try: + country = Country.objects.get(iso_3166_1_a2=kwargs['country_code']) + except Country.DoesNotExist: + country = Country() + + shipping_address = ShippingAddress( + line1=self.request.POST.get('SHIPTOSTREET', ''), + line2=self.request.POST.get('SHIPTOSTREET2', ''), + line4=self.request.POST.get('SHIPTOCITY', ''), + state=self.request.POST.get('SHIPTOSTATE', ''), + postcode=self.request.POST.get('SHIPTOZIP', ''), + country=country + ) + methods = Repository().get_shipping_methods( + basket=basket, shipping_addr=shipping_address, + request=self.request, user=user) + return self.render_to_response(methods, basket) + + + def post(self, request, *args, **kwargs): """ We use the shipping address given to use by PayPal to @@ -431,6 +467,7 @@ def post(self, request, *args, **kwargs): def render_to_response(self, methods, basket): pairs = [ ('METHOD', 'CallbackResponse'), + ('CALLBACKVERSION', '61.0'), ('CURRENCYCODE', self.request.POST.get('CURRENCYCODE', 'GBP')), ] for index, method in enumerate(methods): @@ -449,6 +486,8 @@ def render_to_response(self, methods, basket): else: # No shipping methods available - we flag this up to PayPal indicating that we # do not ship to the shipping address. + pass pairs.append(('NO_SHIPPING_OPTION_DETAILS', 1)) payload = urlencode(pairs) + logger.info(payload) return HttpResponse(payload) From 25aad8d1edd6ded3a29dfaf90a38b7aec1b267cb Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Tue, 9 Apr 2019 23:31:43 +0100 Subject: [PATCH 04/12] Removed setting of no shipping options flag --- paypal/express/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index 1a6149eb..26dda6dc 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -487,7 +487,7 @@ def render_to_response(self, methods, basket): # No shipping methods available - we flag this up to PayPal indicating that we # do not ship to the shipping address. pass - pairs.append(('NO_SHIPPING_OPTION_DETAILS', 1)) + # pairs.append(('NO_SHIPPING_OPTION_DETAILS', 1)) payload = urlencode(pairs) logger.info(payload) return HttpResponse(payload) From 358d4e909daab99ecf179a214f7428a0e3e3a17d Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Wed, 10 Apr 2019 21:51:38 +0100 Subject: [PATCH 05/12] removed some logging, changed another to debug --- paypal/express/views.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index 26dda6dc..4fe48bf4 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -403,8 +403,6 @@ def get(self, request, *args, **kwargs): if not user: user = AnonymousUser() - logger.info(kwargs['country_code']) - # Create a shipping address instance using the data passed back country_code = self.request.POST.get( 'SHIPTOCOUNTRY', None) @@ -489,5 +487,5 @@ def render_to_response(self, methods, basket): pass # pairs.append(('NO_SHIPPING_OPTION_DETAILS', 1)) payload = urlencode(pairs) - logger.info(payload) + logger.debug(payload) return HttpResponse(payload) From 69c6c019853b688cc9aec179bdd4b505dfe8b132 Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Wed, 10 Apr 2019 22:19:31 +0100 Subject: [PATCH 06/12] fix bug with for: else: conditional not working --- paypal/express/views.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index 4fe48bf4..d21e5dc1 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -468,24 +468,25 @@ def render_to_response(self, methods, basket): ('CALLBACKVERSION', '61.0'), ('CURRENCYCODE', self.request.POST.get('CURRENCYCODE', 'GBP')), ] - for index, method in enumerate(methods): - charge = method.calculate(basket).incl_tax - - pairs.append(('L_SHIPPINGOPTIONNAME%d' % index, - six.text_type(method.name))) - pairs.append(('L_SHIPPINGOPTIONLABEL%d' % index, - six.text_type(method.name))) - pairs.append(('L_SHIPPINGOPTIONAMOUNT%d' % index, charge)) - # For now, we assume tax and insurance to be zero - pairs.append(('L_TAXAMT%d' % index, D('0.00'))) - pairs.append(('L_INSURANCEAMT%d' % index, D('0.00'))) - # We assume that the first returned method is the default one - pairs.append(('L_SHIPPINGOPTIONISDEFAULT%d' % index, 1 if index == 0 else 0)) + if methods: + for index, method in enumerate(methods): + charge = method.calculate(basket).incl_tax + + pairs.append(('L_SHIPPINGOPTIONNAME%d' % index, + six.text_type(method.name))) + pairs.append(('L_SHIPPINGOPTIONLABEL%d' % index, + six.text_type(method.name))) + pairs.append(('L_SHIPPINGOPTIONAMOUNT%d' % index, charge)) + # For now, we assume tax and insurance to be zero + pairs.append(('L_TAXAMT%d' % index, D('0.00'))) + pairs.append(('L_INSURANCEAMT%d' % index, D('0.00'))) + # We assume that the first returned method is the default one + pairs.append(('L_SHIPPINGOPTIONISDEFAULT%d' % index, 1 if index == 0 else 0)) else: # No shipping methods available - we flag this up to PayPal indicating that we # do not ship to the shipping address. - pass - # pairs.append(('NO_SHIPPING_OPTION_DETAILS', 1)) + pairs.append(('NO_SHIPPING_OPTION_DETAILS', 1)) + payload = urlencode(pairs) logger.debug(payload) return HttpResponse(payload) From 1d2b2f87378f73719c789ab90e2ffeb34f3838e0 Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Wed, 10 Apr 2019 22:20:15 +0100 Subject: [PATCH 07/12] get method fixed to use request.GET --- paypal/express/views.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index d21e5dc1..25b3589b 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -404,7 +404,7 @@ def get(self, request, *args, **kwargs): user = AnonymousUser() # Create a shipping address instance using the data passed back - country_code = self.request.POST.get( + country_code = self.request.GET.get( 'SHIPTOCOUNTRY', None) try: country = Country.objects.get(iso_3166_1_a2=kwargs['country_code']) @@ -412,11 +412,11 @@ def get(self, request, *args, **kwargs): country = Country() shipping_address = ShippingAddress( - line1=self.request.POST.get('SHIPTOSTREET', ''), - line2=self.request.POST.get('SHIPTOSTREET2', ''), - line4=self.request.POST.get('SHIPTOCITY', ''), - state=self.request.POST.get('SHIPTOSTATE', ''), - postcode=self.request.POST.get('SHIPTOZIP', ''), + line1=self.request.GET.get('SHIPTOSTREET', ''), + line2=self.request.GET.get('SHIPTOSTREET2', ''), + line4=self.request.GET.get('SHIPTOCITY', ''), + state=self.request.GET.get('SHIPTOSTATE', ''), + postcode=self.request.GET.get('SHIPTOZIP', ''), country=country ) methods = Repository().get_shipping_methods( From 433e400cd8d145efd9b696684a81d14a3a9ce0eb Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Wed, 10 Apr 2019 22:37:05 +0100 Subject: [PATCH 08/12] use description rather than duplicating name for L_SHIPPINGOPTIONLABEL, looks neater on paypal --- paypal/express/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index 25b3589b..58052030 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -474,8 +474,9 @@ def render_to_response(self, methods, basket): pairs.append(('L_SHIPPINGOPTIONNAME%d' % index, six.text_type(method.name))) + # this looks horrible in paypal, as the name and label are repeated next to each other pairs.append(('L_SHIPPINGOPTIONLABEL%d' % index, - six.text_type(method.name))) + six.text_type(method.description))) pairs.append(('L_SHIPPINGOPTIONAMOUNT%d' % index, charge)) # For now, we assume tax and insurance to be zero pairs.append(('L_TAXAMT%d' % index, D('0.00'))) From 68b99bf4736eb79c5bc5cc6140a58c60555994b7 Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Wed, 10 Apr 2019 22:38:43 +0100 Subject: [PATCH 09/12] use description rather than duplicating name for L_SHIPPINGOPTIONLABEL, looks neater on paypal --- paypal/express/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index 58052030..56543761 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -474,7 +474,6 @@ def render_to_response(self, methods, basket): pairs.append(('L_SHIPPINGOPTIONNAME%d' % index, six.text_type(method.name))) - # this looks horrible in paypal, as the name and label are repeated next to each other pairs.append(('L_SHIPPINGOPTIONLABEL%d' % index, six.text_type(method.description))) pairs.append(('L_SHIPPINGOPTIONAMOUNT%d' % index, charge)) From 3e287957a460b5ca98e6d1677aac5ddcdfd7918b Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Mon, 15 Apr 2019 10:29:55 +0100 Subject: [PATCH 10/12] Debug log message more explicit to contents --- paypal/express/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index 56543761..0ce740ab 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -488,5 +488,5 @@ def render_to_response(self, methods, basket): pairs.append(('NO_SHIPPING_OPTION_DETAILS', 1)) payload = urlencode(pairs) - logger.debug(payload) + logger.debug("Basket #%s - returning postage costs payload = '%s'", basket.id, payload) return HttpResponse(payload) From 1ef46d1eb35f2a09f216b0ad8a49c36914b1ba28 Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Wed, 1 May 2019 11:17:15 +0100 Subject: [PATCH 11/12] Fix wrap on long line --- paypal/express/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index 0ce740ab..cd2f74b2 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -481,7 +481,8 @@ def render_to_response(self, methods, basket): pairs.append(('L_TAXAMT%d' % index, D('0.00'))) pairs.append(('L_INSURANCEAMT%d' % index, D('0.00'))) # We assume that the first returned method is the default one - pairs.append(('L_SHIPPINGOPTIONISDEFAULT%d' % index, 1 if index == 0 else 0)) + pairs.append(('L_SHIPPINGOPTIONISDEFAULT%d' % index, + 1 if index == 0 else 0)) else: # No shipping methods available - we flag this up to PayPal indicating that we # do not ship to the shipping address. From 225e8b5f4c714819b898013f11f55311f7a4fc2b Mon Sep 17 00:00:00 2001 From: Dorian Fraser-Moore Date: Wed, 1 May 2019 12:06:15 +0100 Subject: [PATCH 12/12] Code style fixes --- paypal/express/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/paypal/express/views.py b/paypal/express/views.py index fb4def03..f4c3c30a 100644 --- a/paypal/express/views.py +++ b/paypal/express/views.py @@ -408,7 +408,7 @@ def get(self, request, *args, **kwargs): country_code = self.request.GET.get( 'SHIPTOCOUNTRY', None) try: - country = Country.objects.get(iso_3166_1_a2=kwargs['country_code']) + country = Country.objects.get(iso_3166_1_a2=country_code) except Country.DoesNotExist: country = Country() @@ -425,8 +425,6 @@ def get(self, request, *args, **kwargs): request=self.request, user=user) return self.render_to_response(methods, basket) - - def post(self, request, *args, **kwargs): """ We use the shipping address given to use by PayPal to @@ -482,7 +480,7 @@ def render_to_response(self, methods, basket): pairs.append(('L_TAXAMT%d' % index, D('0.00'))) pairs.append(('L_INSURANCEAMT%d' % index, D('0.00'))) # We assume that the first returned method is the default one - pairs.append(('L_SHIPPINGOPTIONISDEFAULT%d' % index, + pairs.append(('L_SHIPPINGOPTIONISDEFAULT%d' % index, 1 if index == 0 else 0)) else: # No shipping methods available - we flag this up to PayPal indicating that we