Django Oscar - PayPal Modern Integration - Part 21
 

Content:

 

 

Configuration

Sign up or log in to the PayPal Developer platform: https://developer.paypal.com/home/

  1. Select Sandbox
  2. Select API Credentials
  3. Create App
  4. 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}&currency=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