Merge branch 'release/0.5.2'

This commit is contained in:
Nathan Chapman 2022-03-15 14:34:53 -06:00
commit 7b3b4bacfa
6 changed files with 232 additions and 96 deletions

View File

@ -36,6 +36,15 @@
</div> </div>
</section> </section>
<section class="object__panel">
<div class="object__item object__item--header">
<h4>Shipping</h4>
</div>
<div class="panel__item">
<a href="{% url 'dashboard:order-fulfill' order.pk %}" class="action-button order__fulfill">Ship order &rarr;</a>
</div>
</section>
<section class="object__panel"> <section class="object__panel">
<div class="object__item object__item--header"> <div class="object__item object__item--header">
<h4>Customer</h4> <h4>Customer</h4>
@ -70,10 +79,10 @@
<section class="object__panel"> <section class="object__panel">
<div class="object__item object__item--header"> <div class="object__item object__item--header">
<h4>PayPal</h4> <h4>Transaction</h4>
</div> </div>
<div class="panel__item"> <div class="panel__item">
<p>Transaction: <strong>{{order.transaction.paypal_id}}</strong><br> <p>PayPal transaction ID: <strong>{{order.transaction.paypal_id}}</strong><br>
Status: <strong>{{order.transaction.get_status_display}}</strong> Status: <strong>{{order.transaction.get_status_display}}</strong>
</p> </p>
</div> </div>

View File

@ -24,6 +24,7 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.sites',
# 3rd Party # 3rd Party
'django_filters', 'django_filters',

View File

@ -24,11 +24,11 @@ paypal.Buttons({
}) })
return fetch(request, options) return fetch(request, options)
.then(function(res) { .then(function(res) {
return res.json(); return res.json();
}).then(function(orderData) { }).then(function(orderData) {
return orderData.id; return orderData.id;
}); });
}, },
// Call your server to finalize the transaction // Call your server to finalize the transaction

View File

@ -2,6 +2,7 @@ import logging
from decimal import Decimal from decimal import Decimal
from django.conf import settings from django.conf import settings
from core.models import Product, OrderLine from core.models import Product, OrderLine
from .payments import CreateOrder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -65,59 +66,39 @@ class Cart:
del self.session[settings.CART_SESSION_ID] del self.session[settings.CART_SESSION_ID]
self.session.modified = True self.session.modified = True
def get_bulk_list_and_body_data(self, order, shipping_address=None): def build_order_params(self):
bulk_list = [] 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 = { def create_order(self):
'intent': 'CAPTURE', params = self.build_order_params()
'purchase_units': [{ logger.info(f'\nParams: {params}\n')
'amount': { response = CreateOrder().create_order(params, debug=True)
'currency_code': 'USD', return response
'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': []
}]
}
if shipping_address: def build_bulk_list(self, order):
body_data['purchase_units'][0]['shipping'] = self.process_shipping_address(shipping_address) 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: return bulk_list
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"]}'
})
bulk_list.append( def build_shipping_address(self, address):
OrderLine( return \
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': {
'address_line_1': f'{address["street_address_1"]}', 'address_line_1': f'{address["street_address_1"]}',
'address_line_2': f'{address["street_address_2"]}', 'address_line_2': f'{address["street_address_2"]}',
'admin_area_2': f'{address["city"]}', 'admin_area_2': f'{address["city"]}',
@ -125,8 +106,6 @@ class Cart:
'postal_code': f'{address["postal_code"]}', 'postal_code': f'{address["postal_code"]}',
'country_code': 'US' 'country_code': 'US'
} }
}
return shipping
# @property # @property
# def coupon(self): # def coupon(self):

166
src/storefront/payments.py Normal file
View File

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

View File

@ -22,6 +22,7 @@ from core.forms import ShippingMethodForm
from .forms import AddToCartForm, OrderCreateForm, AddressForm from .forms import AddToCartForm, OrderCreateForm, AddressForm
from .cart import Cart from .cart import Cart
from .payments import CaptureOrder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -91,28 +92,6 @@ class ProductDetailView(FormMixin, DetailView):
template_name = 'storefront/product_detail.html' template_name = 'storefront/product_detail.html'
form_class = AddToCartForm 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): class CheckoutAddressView(FormView):
template_name = 'storefront/checkout_address.html' template_name = 'storefront/checkout_address.html'
@ -177,32 +156,34 @@ class OrderCreateView(CreateView):
return context return context
def form_valid(self, form): def form_valid(self, form):
cart = Cart(self.request)
shipping_address = self.request.session.get('shipping_address') shipping_address = self.request.session.get('shipping_address')
form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address) form.instance.customer, form.instance.shipping_address = get_or_create_customer(self.request, form, shipping_address)
self.object = form.save() self.object = form.save()
bulk_list = cart.build_bulk_list(self.object)
# 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
objs = OrderLine.objects.bulk_create(bulk_list) objs = OrderLine.objects.bulk_create(bulk_list)
# PayPal setup response = cart.create_order()
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)
data = response.result.__dict__['_dict'] data = response.result.__dict__['_dict']
cart.clear() cart.clear()
self.request.session['order_id'] = self.object.id self.request.session['order_id'] = self.object.pk
return JsonResponse(data) 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): class PaymentDoneView(TemplateView):
template_name = 'storefront/payment_done.html' template_name = 'storefront/payment_done.html'