6

I am developing a shopping website using django-oscar framework, in fact I am using their sandbox site. I want to add payment to the checkout process, but the thing is, I am totally confused!

I've read this link : "Oscar's payment integration docs"

and I got the big picture stuff. I also read the file views.py in the checkout app, But I have a few questions which I couldn't find on the web.

My question is what are the methods/classes I should override or create to handle the following process :

after the user requests to make a payment, I should send a request to the bank and give them the required paramteres ( pay_request_parameters in code )

then, they will send an Id, confirming my access and then I should post that Id to an address and redirect the user to the bank's web page.

After the user has payed the bank successfully, they will inform me with a post to the call back url provided by me in the first step.

with that info, I should verify the payment's success and if it was successful, I should ask for a settlement from the bank, to send me the money.

Now the code I have does the first two steps, but I dont know how to handle the process after the redirection in sandbox. here's my code :

from oscar.apps.checkout import views
from oscar.apps.payment import models
from oscar.apps.payment.exceptions import *
import requests
import datetime

mellat_services_url = 'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl'
start_pay_url = 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat'
terminal_id = 'xxx'
username = 'xxx'
password = 'xxx'

# Subclass the core Oscar view so we can customise
class PaymentDetailsView(views.PaymentDetailsView):

    def handle_payment(self, order_number, total, **kwargs):
        # Talk to payment gateway.  If unsuccessful/error, raise a
        # PaymentError exception which we allow to percolate up to be caught
        # and handled by the core PaymentDetailsView.

        # mellat cycle start 
        local_date = str(datetime.date.today())[0:4] + str(datetime.date.today())[5:7] + str(datetime.date.today())[8:10]
        local_time = str(datetime.datetime.now().time())[0:2] + str(datetime.datetime.now().time())[3:5] + str(datetime.datetime.now().time())[6:8]
        # call bpPayRequest and get refId
        pay_request_parameters = {'terminalId': terminal_id, 'userName': username, 
                                'userPassword': password, 'orderId': order_number, 
                                'amount': total.incl_tax, 'localDate': local_date,
                                'localTime': local_time, 'additionalData': ""
                                'callBackUrl': 'mysite.com/checkout/preview/'} 

        pay_request_answer = requests.post(mellat_services_url, pay_request_parameters)

        if not pay_request_answer.split(",")[0] == 0:
            response_code = pay_request_answer.split(",")[0]
            if response_code[0] == '1':
                raise UnableToTakePayment()
            else: 
                raise PaymentError()

        requests.post(start_pay_url, pay_request_answer.split(",")[1])
        raise RedirectRequired(start_pay_url)

        # post the refId to bank and then redirect customer to the bank
        # apparently wait for the bank ( like for 10 mins ) to get the payment status
        # if the bank responded with success, the you verify the payment with a post to the bank
        # if everything was verified, tell the bank for a settlement

        # mellat cycle end

#The rest should be implemented but I dont know where I should put this
#All I know is that it should be done after the verification with the data
#sent from the bank. 

        reference = gateway.pre_auth(order_number, total.incl_tax, kwargs['bankcard'])

        # Payment successful! Record payment source
        source_type, __ = models.SourceType.objects.get_or_create(
            name="SomeGateway")
        source = models.Source(
            source_type=source_type,
            amount_allocated=total.incl_tax,
            reference=reference)
        self.add_payment_source(source)

        # Record payment event
        self.add_payment_event('pre-auth', total.incl_tax)

thanks in advance.

Ashkan Kazemi
  • 1,077
  • 8
  • 26

3 Answers3

3

I had a similar problem, what I did was have the callBackUrl from the bank redirect to a view which is implemented something like this:

class CustomCheckoutDone(OrderPlacementMixin, RedirectView):
"""
here we verify payment was done and place the actual order
then redirect to thank you page
"""
permanent = False

def get_redirect_url(self, pk):
    basket = Basket.objects.get(pk=self.checkout_session.get_submitted_basket_id())
    basket.strategy = CustomStrategy()
    order_number = self.checkout_session.get_order_number()
    shipping_address = self.get_shipping_address(basket)
    shipping_method = self.get_shipping_method(basket, shipping_address)
    shipping_charge = shipping_method.calculate(basket)
    billing_address = self.get_billing_address(shipping_address)
    order_total = self.get_order_totals(basket, shipping_charge=shipping_charge)
    order_kwargs = {}
    # make sure payment was actually paid
    CustomPayment.objects.get(order_number=order_number, payed_sum=str(float(order_total.incl_tax)))
    user = self.request.user
    if not user.is_authenticated():
        order_kwargs['guest_email'] = self.checkout_session.get_guest_email()
    self.handle_order_placement(
        order_number, user, basket, shipping_address, shipping_method,
        shipping_charge, billing_address, order_total, **order_kwargs
    )
    return '/checkout/thank-you/'
Ori Hoch
  • 86
  • 2
  • 1
    Adding to @ori Hoch code, you need to apply offers on the basket using Applicator().apply(basket, request.user, request) – baher Jul 28 '20 at 01:50
3

Well, I had same problem came across solution with the following:

As we redirect to the payment gateway, we should have callback URL for approve, decline and cancel.

First of all, we should add these to settings.py

SESSION_COOKIE_SAMESITE = None
SESSION_COOKIE_DOMAIN = 'localhost' #change it to your domain in prod env.

As the documentation says we need some extra things to do, after payment completed:

https://django-oscar.readthedocs.io/en/2.0.4/howto/how_to_integrate_payment.html

Everything you wrote in the above correct.

Afterwards set your callback URL:
path('gateway/', PaymentReturnURL.as_view(), name="gateway")

from django.shortcuts import render
from oscar.apps.checkout.mixins import OrderPlacementMixin
from django.views.generic.base import TemplateView
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.shortcuts import redirect
from django.urls.base import reverse_lazy
from oscar.core.loading import  get_model
from datetime import datetime
from oscar.apps.payment import models
from xml.etree import ElementTree


Basket = get_model('basket', 'Basket')


@method_decorator(csrf_exempt, name='dispatch')
class PaymentReturnURL(OrderPlacementMixin,TemplateView):
    success_url = reverse_lazy('checkout:thank-you')
    basket_url = reverse_lazy('basket:summary')
    

    def get_success_url(self):
        return self.success_url

    def get_order_details(self):
       
        basket = Basket.objects.get(pk=self.checkout_session.get_submitted_basket_id())
        basket.strategy = self.request.strategy
        order_number = self.checkout_session.get_order_number()
        shipping_address = self.get_shipping_address(basket)
        shipping_method = self.get_shipping_method(basket, shipping_address)
        shipping_charge = shipping_method.calculate(basket)
        billing_address = self.get_billing_address(shipping_address)
        order_total = self.get_order_totals(basket, shipping_charge=shipping_charge)
        order_kwargs = {}

       
        return {
            "basket": basket,
            "order_number": order_number,
            "shipping_address": shipping_address,
            "shipping_method": shipping_method,
            "shipping_charge": shipping_charge,
            "billing_address": billing_address,
            "order_total": order_total,
            "order_kwargs": order_kwargs,
        }

    def get(self, request, *args, **kwargs):

        return redirect(self.get_success_url())

    def post(self, request, *args, **kwargs):
       
        
        #for my case it was XML I needed to parse, after proceding from bank, banks posts the data to your callback url
        context = {}
        data = self.request.POST.get("xmlmsg")
        xml_response = ElementTree.fromstring(data)
        for i in xml_response.iter("*"):
            context[i.tag] = i.text

         
        status = context.get("OrderStatus")
        if status == "APPROVED":
            # Payment successful! Record payment source

            user = self.request.user
            print("the user",user)
            order_details = self.get_order_details()

            
            source_type, __ = models.SourceType.objects.get_or_create(name="Name of Payment")
            source = models.Source(
                source_type=source_type,
                amount_allocated=context.get("PurchaseAmountScr"),
                reference=context.get("OrderID")) 
            
            self.add_payment_source(source)

            # Record payment event
           
            self.add_payment_event('pre-auth', float(context.get("PurchaseAmountScr")))

           
            return self.handle_order_placement(
                order_details['order_number'], user, order_details['basket'], order_details['shipping_address'],
                order_details['shipping_method'], order_details['shipping_charge'],
                order_details['billing_address'], order_details['order_total'], **order_details['order_kwargs']
            )

        #for cancel situation
        elif status == "CANCELED":
            self.restore_frozen_basket()
            return redirect("basket:summary")
         #for decline stiuation
        elif status == "DECLINED":
            self.restore_frozen_basket()
            return redirect("basket:summary")
    def restore_frozen_basket(self):
        """
        Restores a frozen basket as the sole OPEN basket.  Note that this also
        merges in any new products that have been added to a basket that has
        been created while payment.
        """
        try:
           
            fzn_basket = self.get_submitted_basket()
        except Basket.DoesNotExist:
           
            # Strange place.  The previous basket stored in the session does
            # not exist.
            pass
        else:
            fzn_basket.thaw()
            if self.request.basket.id != fzn_basket.id:
                fzn_basket.merge(self.request.basket)
                # Use same strategy as current request basket
                fzn_basket.strategy = self.request.basket.strategy
                self.request.basket = fzn_basket

After your post, you will be redirected to thank you page.

Sabuhi Shukurov
  • 1,616
  • 16
  • 17
0

Another approach: Using the payment service's web hook (IPN, whatever) to verify (and add in the oscar system) the actual payment, and the return URL to finish the order placement process, and then redirect to the thank you page.

Thus, even if a user doesnt come back to the return URL (happens when we close that "you'll be redirected to the merchant in a second" page!), an order will be correctly placed, as we can check the order status in the web hook as well, and finish the order placement first, if needed. The other way round is possible as well (webhook first, returnURL doesn't need to do anything anymore).

I've intentionally not added much of real code, as others have provided this already. I'm still researching how to achieve all of this with as much DRY as possible.

urls.py

path('payment-capture/', PaymentCaptureView.as_view(), name="payment-capture")
path('payment-return/', PaymentReturnView.as_view(), name="payment-return")

views.py

class PaymentCaptureView(OrderPlacementMixin, RedirectView):
    """
    check for successfull payment,
    finish order placement, if needed
    register payment
    notify payment service with 'ok' ;-)=
    """
    permanent = False

    def post(self, pk):
        # check if we actually really received the payment, abort otherwise
        # check if the order is already placed (via return url?)
        # if not, place the order
        # and finally add a the payment in oscar
        return 'ok'


class PaymentReturnView(OrderPlacementMixin, RedirectView):
    """
    finish order placement, redirect to thank you
    """
    permanent = False

    def get_redirect_url(self, pk):
        # check if the order is already placed
        # if not, place the order
        return reverse('checkout:thank-you')
benzkji
  • 1,718
  • 20
  • 41