django-oscar-saferpay 0.1.0
Saferpay payment dashboard for django-oscar.
License: MIT
Keywords: saferpay,payment

# django-oscar-saferpay

*Saferpay dashboard for django-oscar*

This package is a [django-oscar](http://oscarcommerce.com) dashboard component for saferpay.  
First follow the install instructions for django-saferpay ([pypi](https://pypi.org/project/django-saferpay/), [github](https://github.com/taywa/django-saferpay))
.

Install with `pip install django-oscar-saferpay`.

Update your `settings.py`:

```python
INSTALLED_APPS = [
    ...
    'saferpay_oscar',
]
```

in your project `urls.py`:

```python
from saferpay_oscar.dashboard.app import application as saferpay_dashboard_application
...
        url(r'^dashboard/saferpay/', saferpay_dashboard_application.urls),
...
```

add a new payment method to oscar in `settings.py`:

```python
...
OSCAR_PAYMENT_METHODS = (
    ('saferpay', _('Creditcard (Visa, Mastercard, etc..)')),  # all your activated saferpay methods
)
...
```

## Example implementation

Here is an example implementation not meant for for copy & paste. A oscar shop can be setup
totally different.

### Update your checkout `myshop/checkout/app.py`

```python

from django.conf.urls import url
from oscar.apps.checkout import app
from myshop.checkout import views


class CheckoutApplication(app.CheckoutApplication):
    shipping_address_view = views.ShippingAddressView
    shipping_method_view = views.ShippingMethodView
    payment_details_view = views.PaymentDetailsView
    payment_method_view = views.PaymentMethodView
    preview_view = views.PreviewView  # normally just in PaymentDetailsView, custom to this use-case
    payment_capture_view = views.PaymentCapture  # new view not in oscar
    thankyou_view = views.ThankYouView
    payment_failed_view = views.PaymentFailed  # new view not in oscar

    def get_urls(self):
        urls = [
            url(r'^$', self.index_view.as_view(), name='index'),

            # Shipping/user address views
            url(r'shipping-address/$', self.shipping_address_view.as_view(), name='shipping-address'),
            url(r'user-address/edit/(?P<pk>\d+)/$', self.user_address_update_view.as_view(), name='user-address-update'),
            url(r'user-address/delete/(?P<pk>\d+)/$', self.user_address_delete_view.as_view(), name='user-address-delete'),

            # Shipping method views
            url(r'shipping-method/$', self.shipping_method_view.as_view(), name='shipping-method'),

            # Payment views
            url(r'payment-details/$', self.payment_details_view.as_view(), name='payment-details'),
            url(r'payment-method/$', self.payment_method_view.as_view(), name='payment-method'),
            url(r'payment-failed/$', self.payment_failed_view.as_view(), name='payment-failed'),

            # Preview and thankyou
            url(r'preview/$', self.preview_view.as_view(), name='preview'),
            url(r'payment-capture/$', self.payment_capture_view.as_view(), name='payment-capture'),
            url(r'thank-you/$', self.thankyou_view.as_view(), name='thank-you'),
        ]
        return self.post_process_urls(urls)

application = CheckoutApplication()
```

### Update your views

Add it to your custom extended oscar checkout app views.

in `myshop/checkout/views.py`:

```python
...
from saferpay.gateway import SaferpayService
from saferpay import execptions as sp_execptions
...

def billing_address_for_saferpay(billing_address):
    data = {}
    data['FirstName'] = billing_address.first_name
    data['LastName'] = billing_address.last_name
    data['Street'] = billing_address.line1
    if billing_address.line2:
        data['Street2'] = billing_address.line2
    data['Zip'] = billing_address.postcode
    data['City'] = billing_address.city
    return data

# in your PaymentDetailsView in handle_payment()
class PaymentDetailsView(OrderPlacementMixin, generic.TemplateView):
    ...

    def handle_payment(self, order_number, total, **kwargs):
        payment_method = self.checkout_session._get('payment', 'payment_method')
        # invoice
        if payment_method == 'invoice':
            pass
            # nothing to do for invoice, no money collected

        # saferpay
        elif payment_method == 'saferpay
            language_code = translation.get_language()[0:2]
            saferpay_service = SaferpayService(
                order_id=order_number, amount=total.incl_tax, currency=total.currency,
                language_code=language_code
            )
            billing_address = self.get_billing_address(
                shipping_address=self.get_shipping_address(self.request.basket)
            )
            billing_address_data = billing_address_for_saferpay(billing_address)
            payload = saferpay_service.payload_init(billing_address=billing_address_data)

            # redirects to payment page
            try:
                token = saferpay_service.paymentpage_init(payload)
                self.checkout_session._set('payment', 'saferpay_token', token)
                raise RedirectRequired(saferpay_service.paymentpage_redirect().url)
            except sp_execptions.GatewayError as e:
                self.restore_frozen_basket()
                return redirect(reverse_lazy('checkout:payment-failed'))

# new class
class PaymentCapture(views.PaymentDetailsView):
    success_url = reverse_lazy('checkout:thank-you')
    pre_conditions = []

    def load_frozen_basket(self, basket_id):
        # Lookup the frozen basket that this txn corresponds to
        try:
            basket = Basket.objects.get(id=basket_id, status=Basket.FROZEN)
        except Basket.DoesNotExist:
            return None

        if Selector:
            basket.strategy = Selector().strategy(self.request)

        Applicator().apply(basket, self.request.user, request=self.request)

        return basket

    def get(self, request, *args, **kwargs):
        token = self.checkout_session._get('payment', 'saferpay_token')
        saferpay_service = SaferpayService.init_from_transaction(token=token)
        try:
            status = saferpay_service.paymentpage_assert()
        except (
            sp_execptions.GatewayError, sp_execptions.TransactionDeclined,
            sp_execptions.UnableToTakePayment, sp_execptions.PaymentError
        ) as e:
            self.restore_frozen_basket()
            return redirect(reverse_lazy('checkout:payment-failed'))
        if status == 'CAPTURED':
            source_type, is_created = models.SourceType.objects.get_or_create(
                name='saferpay')
            source = source_type.sources.model(
                source_type=source_type,
                amount_allocated=saferpay_service.amount,
                currency=saferpay_service.currency
            )
            self.add_payment_source(source)
            self.add_payment_event('CAPTURED', saferpay_service.amount)

            post_payment.send_robust(sender=self, view=self)

            # If all is ok with payment, try and place order
            logger.info("Order #%s: payment successful, placing order", saferpay_service.order_id)

            try:
                basket_id = self.checkout_session.get_submitted_basket_id()
                basket = self.load_frozen_basket(basket_id)
                shipping_address = self.get_shipping_address(basket)
                billing_address = self.get_billing_address(shipping_address=shipping_address)
                shipping_method = self.get_shipping_method(
                    basket=basket, shipping_address=shipping_address
                )
                shipping_charge = shipping_method.calculate(basket)
                order_total = Price(
                    saferpay_service.currency, saferpay_service.amount, saferpay_service.amount
                )
                order_kwargs = self.checkout_session._get('payment', 'order_kwargs')
                order_number = saferpay_service.order_id
                return self.handle_order_placement(
                    order_number, self.request.user,
                    basket, shipping_address, shipping_method, shipping_charge,
                    billing_address, order_total, **order_kwargs
                )
            except UnableToPlaceOrder as e:
                # It's possible that something will go wrong while trying to
                # actually place an order.
                msg = six.text_type(e)
                logger.error("Order #%s: unable to place order - %s",
                             order_number, msg, exc_info=True)
                self.restore_frozen_basket()
                return self.render_preview(
                    self.request, error=msg
                )

# updated to add payment_method
class ThankYouView(CheckoutSessionMixin, views.ThankYouView):
    def get_context_data(self, **kwargs):
        ctx = super().get_context_data()
        ctx['payment_method'] = self.checkout_session._get('payment', 'payment_method')
        ctx.update(kwargs)
        return ctx

# new class, redirect to preview on payment failure
class PaymentFailed(OrderPlacementMixin, View):
    def get(self, request, *args, **kwargs):
        self.restore_frozen_basket()
        error_txt = _("The payment was not successful, please try again or use a different payment method.")
        messages.error(
            request,
            error_txt
        )
        return redirect(reverse_lazy('checkout:preview'))
```