Skip to content

Commit d310859

Browse files
committed
Implement netaxept gateway
1 parent f4f1a4d commit d310859

30 files changed

+1234
-66
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ test.db
1717
.idea
1818
*.iml
1919

20+
# Code
21+
.vscode/

README.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ This module provides implementations for the following payment-gateways:
5454

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

57+
### Netaxept
58+
Implemented features:
59+
- Authorization
60+
- Capture
61+
- Refund
62+
63+
[More Netaxept information](docs/netaxept.md)
5764

5865
## The example project
5966
The source distribution includes an example project that lets one exercise
@@ -75,7 +82,7 @@ Install the django-payment dependencies (the example project has identical depen
7582

7683
Then point your browser to:
7784

78-
http://127.0.0.1:8000/admin
85+
http://127.0.0.1:8000/admin/
7986

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

@@ -94,7 +101,8 @@ To run unit tests:
94101
pip install pytest-django
95102
pytest
96103

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

99107
pip install tox
100108
tox

devel-requirements.txt

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pytest-django
2+
flake8
3+
mypy
4+
python-language-server
5+

docs/design.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ is impossible to abstract over the myriad ways the different payment gateways do
2323

2424
Here is a diagram of how the different parts interact:
2525

26-
![payment with stripe sequence diagram](payment-with-stripe.png)
26+
![payment with stripe sequence diagram](stripe-authorization.png)
2727

2828

2929
Our changes

docs/netaxept.md

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Netaxept
2+
3+
## Configuration
4+
5+
In the PAYMENT_GATEWAYS setting, configure the netaxept connection params:
6+
7+
`merchant_id`, `secret`, `base_url`, and `after_terminal_url`.
8+
9+
The production base_url is:
10+
11+
`https://epayment.nets.eu/`
12+
13+
14+
## Design
15+
16+
Netaxept works by taking the user to a hosted page and then redirecting the user to the merchant in order to finish
17+
processing the payment.
18+
We chose not to provide such a view in the payment application (we do provide an example view in the example_project),
19+
This means a project that uses netaxept payment will have to implement its own after_terminal view.
20+
21+
- The first reason is that it's not possible to design a simple, generic response that we can show to users of the
22+
application (because we don't know anything about the application)
23+
- The second reason is that after a successful payment something more than just acknowledging the payment
24+
usually needs to happen in the application (for instance setting the status of an order, sending an email, etc).
25+
26+
It's not impossible to solve those two problems with configuration, application-provided functions, and signals
27+
but it doesn't seem like all this complexity is worth it, compared to reimplementing a simple, straightforward webhook.

example_project/settings.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def abspath(*args):
5959

6060
ROOT_URLCONF = 'urls'
6161

62+
DATETIME_FORMAT = "Y-m-d @ H:i:s e"
63+
6264
TEMPLATES = [
6365
{
6466
'BACKEND': 'django.template.backends.django.DjangoTemplates',
@@ -80,14 +82,16 @@ def abspath(*args):
8082
DATETIME_FORMAT = 'Y-m-d @ H:i:s e'
8183

8284
# Enable specific currencies (djmoney)
83-
CURRENCIES = ['USD', 'EUR', 'JPY', 'GBP', 'CAD', 'CHF']
85+
CURRENCIES = ['USD', 'EUR', 'JPY', 'GBP', 'CAD', 'CHF', 'NOK']
8486

8587
DUMMY = 'dummy'
8688
STRIPE = 'stripe'
89+
NETAXEPT = 'netaxept'
8790

8891
CHECKOUT_PAYMENT_GATEWAYS = {
8992
DUMMY: 'Dummy gateway',
9093
STRIPE: 'Stripe',
94+
NETAXEPT: 'Netaxept',
9195
}
9296

9397
PAYMENT_GATEWAYS = {
@@ -117,4 +121,17 @@ def abspath(*args):
117121
},
118122
},
119123
},
124+
NETAXEPT: {
125+
'module': 'payment.gateways.netaxept',
126+
'config': {
127+
'auto_capture': True,
128+
'template_path': 'payment/netaxept.html',
129+
'connection_params': {
130+
'base_url': os.environ.get('NETAXEPT_BASE_URL') or 'https://test.epayment.nets.eu',
131+
'after_terminal_url': 'http://localhost:8000/example/netaxept/after_terminal',
132+
'merchant_id': os.environ.get('NETAXEPT_MERCHANT_ID'),
133+
'secret': os.environ.get('NETAXEPT_SECRET'),
134+
}
135+
}
136+
},
120137
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<html>
2+
<body>
3+
4+
<p>Annulled: {{ query_response.annulled }}</p>
5+
6+
<p>Authorized: {{ query_response.authorized }}</p>
7+
8+
<p>Status code: {{ query_response.raw_response.status_code }}</p>
9+
10+
<p>Raw response:
11+
<pre> {{ query_response.raw_response.text }} </pre>
12+
</p>
13+
14+
</body>
15+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<html>
2+
3+
<body>
4+
5+
<H3>Payments</H3>
6+
7+
{% for payment in payments %}
8+
<div><a href="{%url 'view_payment' payment.id %}">Payment {{payment.id}} - {{payment.total}} ({{payment.gateway}}) </a></div>
9+
{% endfor %}
10+
11+
</body>
12+
</html>

example_project/templates/operation_list.html renamed to example_project/templates/view_payment.html

+6-3
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,20 @@ <H3>Payment</H3>
1212
<li>captured amount: {{ payment.captured_amount }}</li>
1313
</ul>
1414

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

1716
<H3>Operations</H3>
1817
<ul>
1918
{% if payment.gateway == 'stripe' %}
2019
<li><a href="{% url 'stripe_elements_token' payment.id %}">Authorize - Elements token</a></li>
2120
<li><a href="{% url 'stripe_checkout' payment.id %}">Authorize - Checkout</a></li>
2221
<li><a href="{% url 'stripe_payment_intents_manual_flow' payment.id %}">Authorize - Payment intents manual flow</a>
22+
{% elif payment.gateway == 'netaxept' %}
23+
<li><a href="{% url 'netaxept_register_and_goto_terminal' payment.id %}">Register and Goto Terminal</a></li>
24+
{% if payment.token %}
25+
<li><a href="{% url 'netaxept_query' payment.token %}">Query</a></li>
26+
{% endif %}
2327
{% endif %}
24-
25-
<li><a href="{% url 'capture' payment.id %}">Capture</a></li>
28+
<li><a href="{%url 'admin:payment_payment_change' payment.id %}">See payment in admin</a></li>
2629
</ul>
2730

2831
</body>

example_project/urls.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
from django.urls import path, include
33

44
import views
5+
from views import netaxept
56
from views import stripe
67

78
example_urlpatterns = [
9+
path('', views.list_payments, name='list_payments'),
810
path('<payment_id>', views.view_payment, name='view_payment'),
9-
path('<payment_id>/capture', views.capture, name='capture'),
1011
path('stripe/', include(stripe.urls)),
12+
path('netaxept/', include(netaxept.urls)),
1113
]
1214

1315
urlpatterns = [

example_project/views/__init__.py

+6-9
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
from django.http import HttpRequest, HttpResponse
2-
from django.shortcuts import get_object_or_404, redirect
2+
from django.shortcuts import get_object_or_404
33
from django.template.response import TemplateResponse
44
from structlog import get_logger
55

66
from payment.models import Payment
7-
from payment.utils import gateway_capture
87

98
logger = get_logger()
109

1110

12-
def view_payment(request: HttpRequest, payment_id: int) -> HttpResponse:
13-
payment = get_object_or_404(Payment, id=payment_id)
14-
return TemplateResponse(request, 'operation_list.html', {'payment': payment})
11+
def list_payments(request: HttpRequest) -> HttpResponse:
12+
payments = Payment.objects.all()
13+
return TemplateResponse(request, 'payment_list.html', {'payments': payments})
1514

1615

17-
def capture(request: HttpRequest, payment_id: int) -> HttpResponse:
16+
def view_payment(request: HttpRequest, payment_id: int) -> HttpResponse:
1817
payment = get_object_or_404(Payment, id=payment_id)
19-
capture_result = gateway_capture(payment=payment)
20-
logger.info('capture', payment=payment, capture_result=capture_result)
21-
return redirect('view_payment', payment_id=payment_id)
18+
return TemplateResponse(request, 'view_payment.html', {'payment': payment})

example_project/views/netaxept.py

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
Example views for interactive testing of payment with netaxept.
3+
"""
4+
5+
from django.http import HttpRequest
6+
from django.http import HttpResponse
7+
from django.shortcuts import redirect, get_object_or_404
8+
from django.template.response import TemplateResponse
9+
from django.urls import path
10+
from django.views.decorators.http import require_GET
11+
from structlog import get_logger
12+
13+
from payment import get_payment_gateway
14+
from payment.gateways.netaxept import actions
15+
from payment.gateways.netaxept import gateway_to_netaxept_config
16+
from payment.gateways.netaxept import netaxept_protocol
17+
from payment.models import Payment
18+
from payment.utils import gateway_authorize
19+
20+
logger = get_logger()
21+
22+
23+
@require_GET
24+
def register_and_goto_terminal(request: HttpRequest, payment_id: int) -> HttpResponse:
25+
"""
26+
Register the payment with netaxept, and take the user to the terminal page for payment authorization
27+
"""
28+
logger.info('netaxept-register-and-goto-terminal', payment_id=payment_id)
29+
30+
transaction_id = actions.register_payment(payment_id)
31+
32+
payment = get_object_or_404(Payment, id=payment_id)
33+
payment_gateway, gateway_config = get_payment_gateway(payment.gateway)
34+
netaxept_config = gateway_to_netaxept_config(gateway_config)
35+
return redirect(netaxept_protocol.get_payment_terminal_url(config=netaxept_config, transaction_id=transaction_id))
36+
37+
38+
@require_GET
39+
def after_terminal(request):
40+
"""
41+
The browser gets redirected here when the user finishes interacting with the netaxept terminal pages.
42+
We expect query-string parameters: transactionId and responseCode.
43+
44+
We opened the terminal with AutoAuth set to True, so we know that the payment was not merely verified but rather
45+
authorized (we check the veracity of that fact below)
46+
"""
47+
transaction_id = request.GET['transactionId']
48+
response_code = request.GET['responseCode']
49+
logger.info('netaxept-after-terminal', transaction_id=transaction_id, response_code=response_code)
50+
51+
payment = Payment.objects.get(token=transaction_id)
52+
53+
try:
54+
# Here we query netaxept to verify if the payment was indeed authorized: because this endpoint is not secured
55+
# this notification cannot be trusted.
56+
gateway_authorize(payment=payment, payment_token=payment.token)
57+
except Exception as exc:
58+
logger.error('netaxept-after-terminal-error', exc_info=exc)
59+
return HttpResponse('Error authorizing {}: {}'.format(payment.id, exc))
60+
else:
61+
return redirect('view_payment', payment_id=payment.id)
62+
63+
64+
def query(request: HttpRequest, transaction_id: str) -> HttpResponse:
65+
"""
66+
Retries the status of the given transaction from netaxept.
67+
"""
68+
logger.info('netaxept-query', transaction_id=transaction_id)
69+
payment_gateway, gateway_config = get_payment_gateway('netaxept')
70+
netaxept_config = gateway_to_netaxept_config(gateway_config)
71+
query_response = netaxept_protocol.query(config=netaxept_config, transaction_id=transaction_id)
72+
return TemplateResponse(request, 'netaxept/query_result.html', {'query_response': query_response})
73+
74+
75+
urls = [
76+
path('register_and_goto_terminal/<payment_id>', register_and_goto_terminal,
77+
name='netaxept_register_and_goto_terminal'),
78+
path('after_terminal', after_terminal, name='netaxept_after_terminal'),
79+
path('query/<transaction_id>', query, name='netaxept_query'),
80+
]

payment/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class TransactionKind:
5151
"""Represents the type of a transaction.
5252
5353
The following transactions types are possible:
54+
- REGISTER - Some gateways require an initial register transaction, before authorizing.
5455
- AUTH - an amount reserved against the customer's funding source. Money
5556
does not change hands until the authorization is captured.
5657
- VOID - a cancellation of a pending authorization or capture.
@@ -59,6 +60,7 @@ class TransactionKind:
5960
- REFUND - full or partial return of captured funds to the customer.
6061
"""
6162

63+
REGISTER = "register"
6264
AUTH = "auth"
6365
CAPTURE = "capture"
6466
VOID = "void"
@@ -67,6 +69,7 @@ class TransactionKind:
6769
# Which were authorized, but needs to be confirmed manually by staff
6870
# eg. Braintree with "submit_for_settlement" enabled
6971
CHOICES = [
72+
(REGISTER, pgettext_lazy("transaction kind", "Registration")),
7073
(AUTH, pgettext_lazy("transaction kind", "Authorization")),
7174
(REFUND, pgettext_lazy("transaction kind", "Refund")),
7275
(CAPTURE, pgettext_lazy("transaction kind", "Capture")),

0 commit comments

Comments
 (0)