Skip to content

Commit

Permalink
Implement netaxept gateway
Browse files Browse the repository at this point in the history
  • Loading branch information
nwolff committed Oct 11, 2019
1 parent f4f1a4d commit 79b9011
Show file tree
Hide file tree
Showing 26 changed files with 954 additions and 63 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ test.db
.idea
*.iml

# Code
.vscode/
12 changes: 10 additions & 2 deletions 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 All @@ -75,7 +82,7 @@ Install the django-payment dependencies (the example project has identical depen

Then point your browser to:

http://127.0.0.1:8000/admin
http://127.0.0.1:8000/admin/

Create a new payment (make sure the captured amount currency is the same as the total currency)

Expand All @@ -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

27 changes: 27 additions & 0 deletions docs/netaxept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 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/`


## Design

Netaxept works by taking the user to a hosted page and then redirecting the user to the merchant in order to finish
processing the payment.
We chose not to provide such a view in the payment application (we do provide an example view in the example_project),
This means a project that uses netaxept payment will have to implement its own after_terminal view.

- The first reason is that it's not possible to design a simple, generic response that we can show to users of the
application (because we don't know anything about the application)
- The second reason is that after a successful payment something more than just acknowledging the payment
usually needs to happen in the application (for instance setting the status of an order, sending an email, etc).

It's not impossible to solve those two problems with configuration, application-provided functions, and signals
but it doesn't seem like all this complexity is worth it, compared to reimplementing a simple, straightforward webhook.
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': 'http://localhost:8000/example/netaxept/after_terminal',
'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)
66 changes: 66 additions & 0 deletions example_project/views/netaxept.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
Example views for interactive testing of payment with netaxept.
"""
from django.http import HttpRequest
from django.http import HttpResponse
from django.shortcuts import redirect, get_object_or_404
from django.urls import path
from django.views.decorators.http import require_GET
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()


@require_GET
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))


@require_GET
def after_terminal(request):
"""
The browser gets redirected here when the user finishes interacting with the netaxept terminal pages.
We expect query-string parameters: transactionId and responseCode.
See: https://shop.nets.eu/web/partners/response-codes
We know we opened the terminal with AutoAuth set to True, so we interpret this callback to mean that an
AUTH operation was performed. Netaxept does not provide any way to authenticate that the callback really comes
from netaxept (other than them sending us a valid hard to guess 32 character long transaction_id), so we cannot
be 100% sure of the information received.
We decide to store the authorization operation nonetheless. If by any chance the information was faked we will
detect it in the next step, when we try to capture the money.
"""
transaction_id = request.GET['transactionId']
response_code = request.GET['responseCode']
logger.info('netaxept-webhook', transaction_id=transaction_id, response_code=response_code)

success = (response_code == 'OK')

actions.create_auth_transaction(transaction_id=transaction_id, success=success)

if success:
return HttpResponse('ok')
elif response_code:
return HttpResponse('response code {}'.format(response_code))


urls = [
path('register_and_authorize/<payment_id>', register_and_authorize, name='netaxept_register_and_authorize'),
path('after_terminal', after_terminal, name='netaxept_after_terminal'),
]
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
Loading

0 comments on commit 79b9011

Please sign in to comment.