Content:
- Configuration
- The credentials
- The token
- Creating the order
- Capturing the order
- The PayPal button
- Final adjustments
Configuration
Sign up or log in to the PayPal Developer platform: https://developer.paypal.com/home/
- Select Sandbox
- Select API Credentials
- Create App
- Copy/paste Client ID and Secret into mysite/Store/views.py
Allowing popups for PayPal
SECURE_CROSS_ORIGIN_OPENER_POLICY = "same-origin-allow-popups"
The credentials
We can add the paypal_url for sandbox and for live. Then it's a matter of commenting/uncommenting when we go live. We should have something similar to these:
#paypal_url = "https://api-m.paypal.com" # Live
paypal_url = "https://api-m.sandbox.paypal.com" # Sandbox
clientID = "*******"
secret = "*******"
The token
The token is used to authenticate requests to the PayPal API.
It is temporary and expires after a short period, so in production it should be cached and reused instead of requesting a new one for every payment.
import requests
def get_paypal_access_token():
url = paypal_url + "/v1/oauth2/token"
auth = (clientID, secret)
headers = {"Accept": "application/json", "Accept-Language": "en_US"}
data = {"grant_type": "client_credentials"}
response = requests.post(url, headers=headers, data=data, auth=auth)
response.raise_for_status()
return response.json()["access_token"]
Creating the order
In this step, we create a PayPal order from our backend.
We calculate the total amount from the basket and send it to PayPal, which returns an orderID used later during the payment process.
urlpatterns = [
...
path("create-order/", CreatePayPalOrder.as_view(), name="create_order"),
]
from django.http import JsonResponse
class CreatePayPalOrder(CheckoutSessionMixin, RedirectView):
def post(self, *args, **kwargs):
self.request.session["payment_method"] = "PaypalWebhook"
basket = self.build_submission()["basket"]
if basket.is_empty:
messages.error(self.request, "Your basket is empty!")
return reverse("basket:summary")
basket.freeze()
shipping_address = self.get_shipping_address(basket)
shipping_method = self.get_shipping_method(
basket,
shipping_address=shipping_address
)
shipping_charge = shipping_method.calculate(basket)
total = basket.total_incl_tax + shipping_charge.incl_tax
access_token = get_paypal_access_token()
response = requests.post(
f"{paypal_url}/v2/checkout/orders",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json={
"intent": "CAPTURE",
"purchase_units": [
{
"amount": {
"currency_code": "EUR",
"value": f"{total:.2f}"
},
"custom_id": str(basket.id)
}
],
"application_context": {
"return_url": self.request.build_absolute_uri(f"/success/{basket.id}"),
"cancel_url": self.request.build_absolute_uri(f"/cancel/{basket.id}"),
"shipping_preference": "NO_SHIPPING",
"brand_name": "Book Store",
}
}
)
response.raise_for_status()
order = response.json()
return JsonResponse({"orderID": order["id"]})
The custom_id allows us to associate the PayPal transaction with our internal basket or order reference.
Capturing the order
After the user approves the payment, we capture the order using the orderID.
This step finalizes the payment and returns the transaction details from PayPal.
urlpatterns = [
...
path('capture-order/', CapturePayPalOrder.as_view(), name="capture_order"),
]
from django.views.generic import View
import json
class CapturePayPalOrder(View):
def post(self, request, *args, **kwargs):
order_id = json.loads(request.body).get("orderID")
access_token = get_paypal_access_token()
response = requests.post(
f"{paypal_url}/v2/checkout/orders/{order_id}/capture",
headers={
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
},
json={}
)
return JsonResponse(response.json(), status=response.status_code)
The PayPal button
The PayPal button handles the interaction with the user.
It creates the order, allows the user to approve the payment, and then calls our backend to capture it.
<li>
<script src="https://www.paypal.com/sdk/js?client-id={your client id}¤cy=EUR"></script>
<div id="paypal-button-container"></div>
<script>
const basketId = "{{ basket.id }}";
paypal.Buttons({
async createOrder() {
const res = await fetch("/create-order/", {
method: "POST",
headers: {
"X-CSRFToken": "{{ csrf_token }}"
}
});
const data = await res.json();
return data.orderID;
},
async onApprove(data) {
const res = await fetch("/capture-order/", {
method: "POST",
headers: {
"X-CSRFToken": "{{ csrf_token }}",
"Content-Type": "application/json"
},
body: JSON.stringify({
orderID: data.orderID,
basketId: basketId
})
});
const details = await res.json();
if (!res.ok) {
console.error(details);
alert("Error capturing the payment.");
return;
}
window.location.href = "/success/" + basketId + "/";
},
async onCancel(data) {
window.location.href = "/cancel/" + basketId + "/";
}
}).render("#paypal-button-container");
</script>
</li>
Final adjustments
class GatewaySuccess(PaymentDetailsView, OrderPlacementMixin):
...
def handle_payment(self, order_number, total, **kwargs):
reference = "-"
event = "Settled"
payment_method = self.request.session.get('payment_method', 'default_value')
if payment_method == "Paypal2":
reference = self.request.session.get("paypal_capture_id", "-")
event = "PayPal Capture"
source_type, _ = SourceType.objects.get_or_create(name=payment_method)
source = Source(source_type = source_type,
currency = total.currency,
amount_allocated = str(total.incl_tax),
amount_debited = str(total.incl_tax),
reference= reference)
self.add_payment_source(source)
self.add_payment_event(event, str(total.incl_tax), reference=reference)
Tested with: Django 5.2 Django-Oscar 4