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>
</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">
<div class="object__item object__item--header">
<h4>Customer</h4>
@ -70,10 +79,10 @@
<section class="object__panel">
<div class="object__item object__item--header">
<h4>PayPal</h4>
<h4>Transaction</h4>
</div>
<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>
</p>
</div>

View File

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

View File

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

View File

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

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 .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'