From 62349b006eb4cab12f4b440cfce1876d1c7d9155 Mon Sep 17 00:00:00 2001 From: Nathan Chapman Date: Tue, 15 Mar 2022 14:34:32 -0600 Subject: [PATCH] Reconfigure PayPal payment handling --- .../templates/dashboard/order_detail.html | 13 +- src/ptcoffee/settings.py | 1 + src/static/scripts/payment.js | 10 +- src/storefront/cart.py | 81 ++++----- src/storefront/payments.py | 166 ++++++++++++++++++ src/storefront/views.py | 57 ++---- 6 files changed, 232 insertions(+), 96 deletions(-) create mode 100644 src/storefront/payments.py diff --git a/src/dashboard/templates/dashboard/order_detail.html b/src/dashboard/templates/dashboard/order_detail.html index 8e9075c..d20daa9 100644 --- a/src/dashboard/templates/dashboard/order_detail.html +++ b/src/dashboard/templates/dashboard/order_detail.html @@ -36,6 +36,15 @@ +
+
+

Shipping

+
+ +
+

Customer

@@ -70,10 +79,10 @@
-

PayPal

+

Transaction

-

Transaction: {{order.transaction.paypal_id}}
+

PayPal transaction ID: {{order.transaction.paypal_id}}
Status: {{order.transaction.get_status_display}}

diff --git a/src/ptcoffee/settings.py b/src/ptcoffee/settings.py index ed2aef0..88355ea 100644 --- a/src/ptcoffee/settings.py +++ b/src/ptcoffee/settings.py @@ -24,6 +24,7 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.sites', # 3rd Party 'django_filters', diff --git a/src/static/scripts/payment.js b/src/static/scripts/payment.js index 89fdd20..1110467 100644 --- a/src/static/scripts/payment.js +++ b/src/static/scripts/payment.js @@ -24,11 +24,11 @@ paypal.Buttons({ }) return fetch(request, options) - .then(function(res) { - return res.json(); - }).then(function(orderData) { - return orderData.id; - }); + .then(function(res) { + return res.json(); + }).then(function(orderData) { + return orderData.id; + }); }, // Call your server to finalize the transaction diff --git a/src/storefront/cart.py b/src/storefront/cart.py index a60d424..59f8e5e 100644 --- a/src/storefront/cart.py +++ b/src/storefront/cart.py @@ -2,6 +2,7 @@ import logging from decimal import Decimal from django.conf import settings from core.models import Product, OrderLine +from .payments import CreateOrder logger = logging.getLogger(__name__) @@ -65,59 +66,39 @@ class Cart: del self.session[settings.CART_SESSION_ID] self.session.modified = True - def get_bulk_list_and_body_data(self, order, shipping_address=None): - bulk_list = [] + def build_order_params(self): + return \ + { + 'items': self, + 'total_price': f'{self.get_total_price()}', + 'item_total': f'{self.get_total_price()}', + 'shipping_price': '0', + 'tax_total': '0', + 'shipping_method': 'US POSTAL SERVICE', + 'shipping_address': self.build_shipping_address(self.session.get('shipping_address')), + } - body_data = { - 'intent': 'CAPTURE', - 'purchase_units': [{ - 'amount': { - 'currency_code': 'USD', - 'value': f'{self.get_total_price()}', - 'breakdown': { - # Required when including the `items` array - 'item_total': { - 'currency_code': 'USD', - 'value': f'{self.get_total_price()}' - } - } - }, - 'items': [] - }] - } + def create_order(self): + params = self.build_order_params() + logger.info(f'\nParams: {params}\n') + response = CreateOrder().create_order(params, debug=True) + return response - if shipping_address: - body_data['purchase_units'][0]['shipping'] = self.process_shipping_address(shipping_address) + def build_bulk_list(self, order): + bulk_list = [OrderLine( + order=order, + product=item['product'], + customer_note=f'{item["roast"]} {item["other"]}', + unit_price=item['price'], + quantity=item['quantity'], + tax_rate=2, + ) for item in self] - for item in self: - body_data['purchase_units'][0]['items'].append({ - # Shows within upper-right dropdown during payment approval - 'name': f'{item["product"]}', - # Item details will also be in the completed paypal.com transaction view - 'description': 'Coffee', - 'unit_amount': { - 'currency_code': 'USD', - 'value': f'{item["price"]}' - }, - 'quantity': f'{item["quantity"]}' - }) + return bulk_list - bulk_list.append( - OrderLine( - order=order, - product=item['product'], - customer_note=f'{item["roast"]} {item["other"]}', - unit_price=item['price'], - quantity=item['quantity'], - tax_rate=2, - ) - ) - - return bulk_list, body_data - - def process_shipping_address(self, address): - shipping = { - 'address': { + def build_shipping_address(self, address): + return \ + { 'address_line_1': f'{address["street_address_1"]}', 'address_line_2': f'{address["street_address_2"]}', 'admin_area_2': f'{address["city"]}', @@ -125,8 +106,6 @@ class Cart: 'postal_code': f'{address["postal_code"]}', 'country_code': 'US' } - } - return shipping # @property # def coupon(self): diff --git a/src/storefront/payments.py b/src/storefront/payments.py new file mode 100644 index 0000000..1951d4e --- /dev/null +++ b/src/storefront/payments.py @@ -0,0 +1,166 @@ +import os +import sys +import json +import logging + +from paypalcheckoutsdk.core import PayPalHttpClient, SandboxEnvironment +from paypalcheckoutsdk.orders import OrdersCreateRequest, OrdersCaptureRequest +from paypalhttp.serializers.json_serializer import Json + +from django.conf import settings +from django.contrib.sites.models import Site +from django.urls import reverse_lazy + +logger = logging.getLogger(__name__) + +class PayPalClient: + def __init__(self): + self.client_id = settings.PAYPAL_CLIENT_ID + self.client_secret = settings.PAYPAL_SECRET_ID + self.site_domain = Site.objects.get_current().domain + self.site_name = Site.objects.get_current().name + + """Setting up and Returns PayPal SDK environment with PayPal Access credentials. + For demo purpose, we are using SandboxEnvironment. In production this will be + LiveEnvironment.""" + self.environment = SandboxEnvironment(client_id=self.client_id, client_secret=self.client_secret) + + """ Returns PayPal HTTP client instance with environment which has access + credentials context. This can be used invoke PayPal API's provided the + credentials have the access to do so. """ + self.client = PayPalHttpClient(self.environment) + + def object_to_json(self, json_data): + """ + Function to print all json data in an organized readable manner + """ + result = {} + if sys.version_info[0] < 3: + itr = json_data.__dict__.iteritems() + else: + itr = json_data.__dict__.items() + for key,value in itr: + # Skip internal attributes. + if key.startswith("__") or key.startswith("_"): + continue + result[key] = self.array_to_json_array(value) if isinstance(value, list) else\ + self.object_to_json(value) if not self.is_primittive(value) else\ + value + return result + + def array_to_json_array(self, json_array): + result =[] + if isinstance(json_array, list): + for item in json_array: + result.append(self.object_to_json(item) if not self.is_primittive(item) \ + else self.array_to_json_array(item) if isinstance(item, list) else item) + return result + + def is_primittive(self, data): + return isinstance(data, str) or isinstance(data, unicode) or isinstance(data, int) + + +class CreateOrder(PayPalClient): + + """Setting up the JSON request body for creating the Order. The Intent in the + request body should be set as "CAPTURE" for capture intent flow.""" + + def build_request_body(self, params): + """Method to create body with CAPTURE intent""" + processed_items = [{ + # Shows within upper-right dropdown during payment approval + 'name': f'{item["product"]}', + # Item details will also be in the completed paypal.com transaction view + 'description': 'Coffee', + 'unit_amount': { + 'currency_code': 'USD', + 'value': f'{item["price"]}' + }, + 'quantity': f'{item["quantity"]}' + } for item in params['items']] + + request_body = { + "intent": "CAPTURE", + "application_context": { + "return_url": f"https://{self.site_domain}{reverse_lazy('storefront:payment-done')}", + "cancel_url": f"https://{self.site_domain}{reverse_lazy('storefront:payment-canceled')}", + "brand_name": f"{self.site_name}", + # "landing_page": "BILLING", + # "shipping_preference": "SET_PROVIDED_ADDRESS", + # "user_action": "CONTINUE" + }, + "purchase_units": [ + { + # "reference_id": "PUHF", + "description": "Coffee", + + # "custom_id": "CUST-HighFashions", + # "soft_descriptor": "HighFashions", + "amount": { + "currency_code": "USD", + "value": params['total_price'], + "breakdown": { + "item_total": { + "currency_code": "USD", + "value": params['item_total'] + }, + "shipping": { + "currency_code": "USD", + "value": params['shipping_price'] + }, + "tax_total": { + "currency_code": "USD", + "value": params['tax_total'] + }, + # "shipping_discount": { + # "currency_code": "USD", + # "value": "10" + # } + } + }, + "items": processed_items, + "shipping": { + "method": params['shipping_method'], + "address": params['shipping_address'] + } + } + ] + } + + logger.info(f'\nRequest body: {request_body}\n') + + return request_body + + """ This is the sample function which can be sued to create an order. It uses the + JSON body returned by buildRequestBody() to create an new Order.""" + + def create_order(self, params, debug=False): + request = OrdersCreateRequest() + request.headers['prefer'] = 'return=representation' + request.request_body(self.build_request_body(params)) + response = self.client.execute(request) + if debug: + logger.info(f'\nStatus Code: {response.status_code}', ) + logger.info(f'\nStatus: {response.result.status}', ) + logger.info(f'\nOrder ID: {response.result.id}', ) + logger.info(f'\nIntent: {response.result.intent}', ) + logger.info(f"\njson_data: {response.result}") + + return response + +class CaptureOrder(PayPalClient): + + """this is the sample function performing payment capture on the order. Approved Order id should be passed as an argument to this function""" + + def capture_order(self, order_id, debug=False): + """Method to capture order using order_id""" + request = OrdersCaptureRequest(order_id) + response = self.client.execute(request) + + data = response.result.__dict__['_dict'] + data['redirect_urls'] = { + 'return_url': f"https://{self.site_domain}{reverse_lazy('storefront:payment-done')}", + 'cancel_url': f"https://{self.site_domain}{reverse_lazy('storefront:payment-canceled')}" + } + + return data diff --git a/src/storefront/views.py b/src/storefront/views.py index 8bb66b8..4fd7fbe 100644 --- a/src/storefront/views.py +++ b/src/storefront/views.py @@ -22,6 +22,7 @@ from core.forms import ShippingMethodForm from .forms import AddToCartForm, OrderCreateForm, AddressForm from .cart import Cart +from .payments import CaptureOrder logger = logging.getLogger(__name__) @@ -91,28 +92,6 @@ class ProductDetailView(FormMixin, DetailView): template_name = 'storefront/product_detail.html' form_class = AddToCartForm -def paypal_order_transaction_capture(request, transaction_id): - if request.method =="POST": - capture_order = OrdersCaptureRequest(transaction_id) - environment = SandboxEnvironment(client_id=settings.PAYPAL_CLIENT_ID, client_secret=settings.PAYPAL_SECRET_ID) - client = PayPalHttpClient(environment) - - response = client.execute(capture_order) - data = response.result.__dict__['_dict'] - data['redirect_urls'] = { - 'return_url': request.build_absolute_uri(reverse_lazy('storefront:payment-done')), - 'cancel_url': request.build_absolute_uri(reverse_lazy('storefront:payment-canceled')) - } - transaction = Transaction.objects.get(order__pk=request.session.get('order_id')) - transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id'] - transaction.status = data['status'] - transaction.save() - logger.debug(f'\nPayPal Response data: {data}\n') - - return JsonResponse(data) - else: - return JsonResponse({'details': 'invalid request'}) - class CheckoutAddressView(FormView): template_name = 'storefront/checkout_address.html' @@ -177,32 +156,34 @@ class OrderCreateView(CreateView): return context def form_valid(self, form): + cart = Cart(self.request) shipping_address = self.request.session.get('shipping_address') form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address) self.object = form.save() - - # Cart data setup - cart = Cart(self.request) - bulk_list, body_data = cart.get_bulk_list_and_body_data(self.object, shipping_address) - logger.debug(f'\nBody data: {body_data}\n') - - # Bulk create OrderLine objects from cart items + bulk_list = cart.build_bulk_list(self.object) objs = OrderLine.objects.bulk_create(bulk_list) - # PayPal setup - environment = SandboxEnvironment(client_id=settings.PAYPAL_CLIENT_ID, client_secret=settings.PAYPAL_SECRET_ID) - client = PayPalHttpClient(environment) - create_order = OrdersCreateRequest() - create_order.request_body(body_data) - - response = client.execute(create_order) + response = cart.create_order() data = response.result.__dict__['_dict'] - cart.clear() - self.request.session['order_id'] = self.object.id + self.request.session['order_id'] = self.object.pk return JsonResponse(data) +def paypal_order_transaction_capture(request, transaction_id): + if request.method =="POST": + data = CaptureOrder().capture_order(transaction_id) + + transaction = Transaction.objects.get(order__pk=request.session.get('order_id')) + transaction.paypal_id = data['purchase_units'][0]['payments']['captures'][0]['id'] + transaction.status = data['status'] + transaction.save() + logger.debug(f'\nPayPal Response data: {data}\n') + + return JsonResponse(data) + else: + return JsonResponse({'details': 'invalid request'}) + class PaymentDoneView(TemplateView): template_name = 'storefront/payment_done.html'