Skip to content

Commit

Permalink
Implement netaxept gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
nwolff committed Sep 23, 2019
1 parent f4f1a4d commit 5b0bf46
Show file tree
Hide file tree
Showing 27 changed files with 951 additions and 61 deletions.
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ This module provides implementations for the following payment-gateways:

[More Stripe information](docs/stripe.md)

### Netaxept
Implemented features:
- Authorization
- Capture
- Refund

[More Netaxept information](docs/netaxept.md)

## The example project
The source distribution includes an example project that lets one exercise
Expand Down Expand Up @@ -94,7 +101,8 @@ To run unit tests:
pip install pytest-django
pytest

To lint, typecheck, test, and verify you didn't forget to create a migration:
To lint, typecheck, test on all supported versions of python and django.
Also to verify you didn't forget to create a migration:

pip install tox
tox
Expand Down
5 changes: 5 additions & 0 deletions devel-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pytest-django
flake8
mypy
python-language-server

13 changes: 13 additions & 0 deletions docs/netaxept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Netaxept

## Configuration

In the PAYMENT_GATEWAYS setting, configure the netaxept connection params:

`merchant_id`, `secret`, `base_url`, and `after_terminal_url`.

The production base_url is:

`https://epayment.nets.eu/`


19 changes: 18 additions & 1 deletion example_project/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ def abspath(*args):

ROOT_URLCONF = 'urls'

DATETIME_FORMAT = "Y-m-d @ H:i:s e"

TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
Expand All @@ -80,14 +82,16 @@ def abspath(*args):
DATETIME_FORMAT = 'Y-m-d @ H:i:s e'

# Enable specific currencies (djmoney)
CURRENCIES = ['USD', 'EUR', 'JPY', 'GBP', 'CAD', 'CHF']
CURRENCIES = ['USD', 'EUR', 'JPY', 'GBP', 'CAD', 'CHF', 'NOK']

DUMMY = 'dummy'
STRIPE = 'stripe'
NETAXEPT = 'netaxept'

CHECKOUT_PAYMENT_GATEWAYS = {
DUMMY: 'Dummy gateway',
STRIPE: 'Stripe',
NETAXEPT: 'Netaxept',
}

PAYMENT_GATEWAYS = {
Expand Down Expand Up @@ -117,4 +121,17 @@ def abspath(*args):
},
},
},
NETAXEPT: {
'module': 'payment.gateways.netaxept',
'config': {
'auto_capture': True,
'template_path': 'payment/netaxept.html',
'connection_params': {
'base_url': os.environ.get('NETAXEPT_BASE_URL') or 'https://test.epayment.nets.eu',
'after_terminal_url': os.environ.get('NETAXEPT_AFTER_TERMINAL_URL'),
'merchant_id': os.environ.get('NETAXEPT_MERCHANT_ID'),
'secret': os.environ.get('NETAXEPT_SECRET'),
}
}
},
}
6 changes: 3 additions & 3 deletions example_project/templates/operation_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ <H3>Payment</H3>
<li>captured amount: {{ payment.captured_amount }}</li>
</ul>

<a href="{%url 'admin:payment_payment_change' payment.id %}">See payment in admin </a>

<H3>Operations</H3>
<ul>
{% if payment.gateway == 'stripe' %}
<li><a href="{% url 'stripe_elements_token' payment.id %}">Authorize - Elements token</a></li>
<li><a href="{% url 'stripe_checkout' payment.id %}">Authorize - Checkout</a></li>
<li><a href="{% url 'stripe_payment_intents_manual_flow' payment.id %}">Authorize - Payment intents manual flow</a>
{% elif payment.gateway == 'netaxept' %}
<li><a href="{% url 'netaxept_register_and_authorize' payment.id %}">Register and Authorize</a></li>
{% endif %}

<li><a href="{% url 'capture' payment.id %}">Capture</a></li>
<li><a href="{%url 'admin:payment_payment_change' payment.id %}">See payment in admin</a></li>
</ul>

</body>
Expand Down
3 changes: 2 additions & 1 deletion example_project/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from django.urls import path, include

import views
from views import netaxept
from views import stripe

example_urlpatterns = [
path('<payment_id>', views.view_payment, name='view_payment'),
path('<payment_id>/capture', views.capture, name='capture'),
path('stripe/', include(stripe.urls)),
path('netaxept/', include(netaxept.urls)),
]

urlpatterns = [
Expand Down
10 changes: 1 addition & 9 deletions example_project/views/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from structlog import get_logger

from payment.models import Payment
from payment.utils import gateway_capture

logger = get_logger()


def view_payment(request: HttpRequest, payment_id: int) -> HttpResponse:
payment = get_object_or_404(Payment, id=payment_id)
return TemplateResponse(request, 'operation_list.html', {'payment': payment})


def capture(request: HttpRequest, payment_id: int) -> HttpResponse:
payment = get_object_or_404(Payment, id=payment_id)
capture_result = gateway_capture(payment=payment)
logger.info('capture', payment=payment, capture_result=capture_result)
return redirect('view_payment', payment_id=payment_id)
36 changes: 36 additions & 0 deletions example_project/views/netaxept.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""
Example views for interactive testing of payment with netaxept.
You should restrict access (maybe with 'staff_member_required') if you choose to add this to your urlconf.
"""
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect, get_object_or_404
from django.urls import path
from structlog import get_logger

from payment import get_payment_gateway
from payment.gateways.netaxept import actions
from payment.gateways.netaxept import gateway_to_netaxept_config
from payment.gateways.netaxept.netaxept_protocol import get_payment_terminal_url
from payment.models import Payment

logger = get_logger()


def register_and_authorize(request: HttpRequest, payment_id: int) -> HttpResponse:
"""
Register the payment with netaxept, and take the user to the terminal page for payment authorization.
"""
logger.info('netaxept-register-and-authorize', payment_id=payment_id)

transaction_id = actions.register_payment(payment_id)

payment = get_object_or_404(Payment, id=payment_id)
payment_gateway, gateway_config = get_payment_gateway(payment.gateway)
netaxept_config = gateway_to_netaxept_config(gateway_config)
return redirect(get_payment_terminal_url(config=netaxept_config, transaction_id=transaction_id))


urls = [
path('register_and_authorize/<payment_id>', register_and_authorize, name='netaxept_register_and_authorize'),
]
3 changes: 3 additions & 0 deletions payment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class TransactionKind:
"""Represents the type of a transaction.
The following transactions types are possible:
- REGISTER - Some gateways require an initial register transaction, before authorizing.
- AUTH - an amount reserved against the customer's funding source. Money
does not change hands until the authorization is captured.
- VOID - a cancellation of a pending authorization or capture.
Expand All @@ -59,6 +60,7 @@ class TransactionKind:
- REFUND - full or partial return of captured funds to the customer.
"""

REGISTER = "register"
AUTH = "auth"
CAPTURE = "capture"
VOID = "void"
Expand All @@ -67,6 +69,7 @@ class TransactionKind:
# Which were authorized, but needs to be confirmed manually by staff
# eg. Braintree with "submit_for_settlement" enabled
CHOICES = [
(REGISTER, pgettext_lazy("transaction kind", "Registration")),
(AUTH, pgettext_lazy("transaction kind", "Authorization")),
(REFUND, pgettext_lazy("transaction kind", "Refund")),
(CAPTURE, pgettext_lazy("transaction kind", "Capture")),
Expand Down
100 changes: 83 additions & 17 deletions payment/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from djmoney.forms import MoneyField
from import_export.admin import ExportMixin
Expand All @@ -13,7 +14,7 @@

from .export import PaymentResource
from .models import Payment, Transaction
from .utils import gateway_refund
from .utils import gateway_refund, gateway_void, gateway_capture


##############################################################
Expand Down Expand Up @@ -77,6 +78,31 @@ def has_module_permission(self, request):
##############################################################
# Payments

class CapturePaymentForm(forms.Form):
amount = MoneyField(min_value=0)


def capture_payment_form(request, payment_id):
payment = get_object_or_404(Payment, pk=payment_id)
if request.method == 'POST':
form = CapturePaymentForm(request.POST)
if form.is_valid():
gateway_capture(payment=payment, amount=form.cleaned_data['amount'])
# As confirmation we take the user to the payment page
return HttpResponseRedirect(reverse('admin:payment_payment_change', args=[payment_id]))
else:
form = CapturePaymentForm(initial={'amount': payment.get_charge_amount()})

return render(
request,
'admin/payment/form.html',
{
'title': 'Capture in {}, for payment with total: {}'.format(payment.gateway, payment.total),
'form': form,
'opts': Payment._meta, # Used to setup the navigation / breadcrumbs of the page
}
)


class RefundPaymentForm(forms.Form):
amount = MoneyField(min_value=0)
Expand All @@ -87,9 +113,7 @@ def refund_payment_form(request, payment_id):
if request.method == 'POST':
form = RefundPaymentForm(request.POST)
if form.is_valid():
gateway_refund(
payment=payment,
amount=form.cleaned_data['amount'])
gateway_refund(payment=payment, amount=form.cleaned_data['amount'])
# As confirmation we take the user to the payment page
return HttpResponseRedirect(reverse('admin:payment_payment_change', args=[payment_id]))
else:
Expand All @@ -106,6 +130,32 @@ def refund_payment_form(request, payment_id):
)


class VoidPaymentForm(forms.Form):
pass


def void_payment_form(request, payment_id):
payment = get_object_or_404(Payment, pk=payment_id)
if request.method == 'POST':
form = VoidPaymentForm(request.POST)
if form.is_valid():
gateway_void(payment=payment)
# As confirmation we take the user to the payment page
return HttpResponseRedirect(reverse('admin:payment_payment_change', args=[payment_id]))
else:
form = VoidPaymentForm()

return render(
request,
'admin/payment/form.html',
{
'title': 'Void in {}, for payment with total: {}'.format(payment.gateway, payment.total),
'form': form,
'opts': Payment._meta, # Used to setup the navigation / breadcrumbs of the page
}
)


@admin.register(Payment)
class PaymentAdmin(ExportMixin, admin.ModelAdmin):
date_hierarchy = 'created'
Expand All @@ -115,7 +165,7 @@ class PaymentAdmin(ExportMixin, admin.ModelAdmin):
'customer_email']
search_fields = ['customer_email', 'token', 'total', 'id']

readonly_fields = ['created', 'modified', 'refund_button']
readonly_fields = ['created', 'modified', 'operation_button']
inlines = [TransactionInline]

resource_class = PaymentResource
Expand All @@ -135,20 +185,36 @@ def formatted_captured_amount(self, obj):
def get_urls(self):
urls = super().get_urls()
my_urls = [
url(
r'^(?P<payment_id>[0-9a-f-]+)/refund/$',
url(r'^(?P<payment_id>[0-9a-f-]+)/refund/$',
self.admin_site.admin_view(refund_payment_form),
name='payment_refund',
),
name='payment_refund'),
url(r'^(?P<payment_id>[0-9a-f-]+)/void/$',
self.admin_site.admin_view(void_payment_form),
name='payment_void'),
url(r'^(?P<payment_id>[0-9a-f-]+)/capture/$',
self.admin_site.admin_view(capture_payment_form),
name='payment_capture'),
]
return my_urls + urls

def refund_button(self, payment):
def operation_button(self, payment):
buttons = []
if payment.can_capture():
buttons.append(format_html(
'<a class="button" href="{}">{}</a>',
reverse('admin:payment_capture', args=[payment.pk]),
_('Capture')))
if payment.can_refund():
return format_html('<a class="button" href="{}">{}</a>',
reverse('admin:payment_refund', args=[payment.pk]),
_('Refund'))
else:
return '-'

refund_button.short_description = _('Refund') # type: ignore
buttons.append(format_html(
'<a class="button" href="{}">{}</a>',
reverse('admin:payment_refund', args=[payment.pk]),
_('Refund')))
if payment.can_void():
buttons.append(format_html(
'<a class="button" href="{}">{}</a>',
reverse('admin:payment_void', args=[payment.pk]),
_('Void')))

return mark_safe('&nbsp;&nbsp;'.join(buttons)) if buttons else '-'

operation_button.short_description = _('Operation') # type: ignore
Loading

0 comments on commit 5b0bf46

Please sign in to comment.