Merge branch 'release/0.5.2'
This commit is contained in:
commit
7b3b4bacfa
@ -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 →</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>
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
166
src/storefront/payments.py
Normal 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
|
||||||
@ -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'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user