From 9068f9759270980dc22561e1a14a53a4495a5c0b Mon Sep 17 00:00:00 2001
From: Nicholas Wolff
Date: Mon, 14 Oct 2019 09:29:17 +0200
Subject: [PATCH] query
---
.../templates/netaxept/query_result.html | 13 ++
example_project/templates/operation_list.html | 3 +
example_project/views/netaxept.py | 36 +++---
payment/gateways/netaxept/actions.py | 51 ++++++--
.../gateways/netaxept/netaxept_protocol.py | 64 ++++++++--
tests/gateways/test_netaxept.py | 112 ++++++++++++++++--
6 files changed, 229 insertions(+), 50 deletions(-)
create mode 100644 example_project/templates/netaxept/query_result.html
diff --git a/example_project/templates/netaxept/query_result.html b/example_project/templates/netaxept/query_result.html
new file mode 100644
index 0000000..7155071
--- /dev/null
+++ b/example_project/templates/netaxept/query_result.html
@@ -0,0 +1,13 @@
+
+
+
+Annuled: {{ query_response.annuled }}
+
+Authorized: {{ query_response.authorized }}
+
+Status code: {{ query_response.raw_response.status_code }}
+
+Raw response:
{{ query_response.raw_response.text }}
+
+
+
diff --git a/example_project/templates/operation_list.html b/example_project/templates/operation_list.html
index 4206c31..b713084 100644
--- a/example_project/templates/operation_list.html
+++ b/example_project/templates/operation_list.html
@@ -21,6 +21,9 @@ Operations
Authorize - Payment intents manual flow
{% elif payment.gateway == 'netaxept' %}
Register and Authorize
+ {% if payment.token %}
+ Query
+ {% endif %}
{% endif %}
See payment in admin
diff --git a/example_project/views/netaxept.py b/example_project/views/netaxept.py
index a48e715..f1982bb 100644
--- a/example_project/views/netaxept.py
+++ b/example_project/views/netaxept.py
@@ -1,9 +1,11 @@
"""
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.template.response import TemplateResponse
from django.urls import path
from django.views.decorators.http import require_GET
from structlog import get_logger
@@ -11,7 +13,7 @@
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.gateways.netaxept import netaxept_protocol
from payment.models import Payment
logger = get_logger()
@@ -29,7 +31,7 @@ def register_and_authorize(request: HttpRequest, payment_id: int) -> HttpRespons
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))
+ return redirect(netaxept_protocol.get_payment_terminal_url(config=netaxept_config, transaction_id=transaction_id))
@require_GET
@@ -39,28 +41,32 @@ def after_terminal(request):
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.
+ We opened the terminal with AutoAuth set to True, so this callback should mean an AUTH operation was performed
+ (and the response_code should indicate the outcome of the operation).
+ But we cannot rely on the information received (because netaxept does not include any digital signature),
+ so we go on and verify if the authorization was succesful.
"""
transaction_id = request.GET['transactionId']
response_code = request.GET['responseCode']
- logger.info('netaxept-webhook', transaction_id=transaction_id, response_code=response_code)
+ logger.info('netaxept-after-terminal', transaction_id=transaction_id, response_code=response_code)
+
+ payment_authorized = actions.verify_auth_transaction(transaction_id=transaction_id, response_code=response_code)
- success = (response_code == 'OK')
+ return HttpResponse('transaction id {}, response code {}, payment authorized {}'
+ .format(transaction_id, response_code, payment_authorized))
- 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))
+def query(request: HttpRequest, transaction_id: str) -> HttpResponse:
+ """ Queries netaxept for the details of the transaction related to the given payment_id """
+ logger.info('netaxept-query', transaction_id=transaction_id)
+ payment_gateway, gateway_config = get_payment_gateway('netaxept')
+ netaxept_config = gateway_to_netaxept_config(gateway_config)
+ query_response = netaxept_protocol.query(config=netaxept_config, transaction_id=transaction_id)
+ return TemplateResponse(request, 'netaxept/query_result.html', {'query_response': query_response})
urls = [
path('register_and_authorize/', register_and_authorize, name='netaxept_register_and_authorize'),
path('after_terminal', after_terminal, name='netaxept_after_terminal'),
+ path('query/', query, name='netaxept_query'),
]
diff --git a/payment/gateways/netaxept/actions.py b/payment/gateways/netaxept/actions.py
index eb7c31f..73c1fac 100644
--- a/payment/gateways/netaxept/actions.py
+++ b/payment/gateways/netaxept/actions.py
@@ -1,11 +1,14 @@
from django.db import transaction
from django.shortcuts import get_object_or_404
+from structlog import get_logger
from payment import get_payment_gateway, TransactionKind
from payment.gateways.netaxept import NetaxeptProtocolError
from payment.gateways.netaxept import netaxept_protocol, gateway_to_netaxept_config
from payment.models import Payment, Transaction
+logger = get_logger()
+
class NetaxeptException(Exception):
def __str__(self):
@@ -26,6 +29,8 @@ def register_payment(payment_id: int) -> str:
:return: The newly created netaxept transaction_id
:raises NetaxeptException: If the registration fails
"""
+ logger.info('netaxept-actions-register', payment_id=payment_id)
+
payment = get_object_or_404(Payment, id=payment_id)
if payment.token != '':
@@ -39,7 +44,8 @@ def register_payment(payment_id: int) -> str:
config=netaxept_config,
order_number=payment_id,
amount=payment.total,
- language='en')
+ language='en',
+ customer_email=payment.customer_email)
except NetaxeptProtocolError as exception:
Transaction.objects.create(
payment=payment,
@@ -52,6 +58,9 @@ def register_payment(payment_id: int) -> str:
raise NetaxeptException(exception.error)
with transaction.atomic():
+ payment.token = register_response.transaction_id
+ payment.save()
+
Transaction.objects.create(
payment=payment,
kind=TransactionKind.REGISTER,
@@ -61,21 +70,43 @@ def register_payment(payment_id: int) -> str:
error=None,
gateway_response=register_response.raw_response)
- payment.token = register_response.transaction_id
- payment.save()
-
return register_response.transaction_id
-def create_auth_transaction(transaction_id: str, success: bool) -> Transaction:
- """ Record the outcome of a netaxept auth transaction. """
+def verify_auth_transaction(transaction_id: str, response_code: str) -> bool:
+ """
+ Verify and record the outcome of a netaxept auth transaction.
+
+ :param transaction_id:
+ :param response_code:
+ :return:
+ """
+ logger.info('netaxept-actions-verify-auth-transaction', transaction_id=transaction_id, response_code=response_code)
+
payment = Payment.objects.get(token=transaction_id)
- return Transaction.objects.create(
+ _payment_gateway, gateway_config = get_payment_gateway('netaxept')
+ netaxept_config = gateway_to_netaxept_config(gateway_config)
+
+ try:
+ query_response = netaxept_protocol.query(config=netaxept_config, transaction_id=transaction_id)
+ transaction_authorized = query_response.authorized
+ error = None
+ except NetaxeptProtocolError as exception:
+ transaction_authorized = False
+ error = exception
+
+ if response_code == '' and not transaction_authorized:
+ logger.error('') # XXX We were lied-to
+
+ # Record the outcome whether the authorization succeeded or not
+ Transaction.objects.create(
payment=payment,
kind=TransactionKind.AUTH,
token=transaction_id,
- is_success=success,
+ is_success=transaction_authorized,
amount=payment.total,
- error=None,
- gateway_response={})
+ error=error,
+ gateway_response={'response_code': response_code})
+
+ return transaction_authorized
diff --git a/payment/gateways/netaxept/netaxept_protocol.py b/payment/gateways/netaxept/netaxept_protocol.py
index 7686f68..63f7c8b 100644
--- a/payment/gateways/netaxept/netaxept_protocol.py
+++ b/payment/gateways/netaxept/netaxept_protocol.py
@@ -14,16 +14,16 @@
API details: https://shop.nets.eu/web/partners/appi
Test card numbers: https://shop.nets.eu/web/partners/test-cards
"""
+from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
+from typing import Optional, Union, Dict, Any
from urllib.parse import urlencode, urljoin
import requests
import xmltodict
-from dataclasses import dataclass
from moneyed import Money
from structlog import get_logger
-from typing import Optional, Union, Dict
logger = get_logger()
@@ -54,11 +54,12 @@ def __init__(self, error: str, raw_response: Dict[str, str]):
@dataclass
class RegisterResponse:
transaction_id: str
- raw_response: Dict[str, str]
+ raw_response: Dict[str, Any]
def register(config: NetaxeptConfig, amount: Money, order_number: Union[str, int],
- language: Optional[str] = None, description: Optional[str] = None) -> RegisterResponse:
+ language: Optional[str] = None, description: Optional[str] = None,
+ customer_email: Optional[str] = None) -> RegisterResponse:
"""
Registering a payment is the first step for netaxept, before taking the user to the netaxept
terminal hosted page.
@@ -70,12 +71,13 @@ def register(config: NetaxeptConfig, amount: Money, order_number: Union[str, int
:param order_number: An alphanumerical string identifying the payment. 32 chars max (letters and numbers)
:param language: The iso639-1 code of the language in which the terminal should be displayed.
:param description: A text that will be displayed in the netaxept admin (but not to the user).
+ :param customer_email: The email of the customer, can then be seen in the netaxept admin portal.
:return: a RegisterResponse
:raises: NetaxeptProtocolError
"""
logger.info('netaxept-register', amount=amount, order_number=order_number, language=language,
- description=description)
+ description=description, customer_email=customer_email)
params = {
'merchantId': config.merchant_id,
@@ -94,10 +96,11 @@ def register(config: NetaxeptConfig, amount: Money, order_number: Union[str, int
'redirectUrl': config.after_terminal_url
}
- response = requests.post(url=urljoin(config.base_url, 'Netaxept/Register.aspx'),
- data=params)
- raw_response = _build_raw_response(response)
+ if customer_email is not None:
+ params['customerEmail'] = customer_email
+ response = requests.post(url=urljoin(config.base_url, 'Netaxept/Register.aspx'), data=params)
+ raw_response = _build_raw_response(response)
logger.info('netaxept-register', amount=amount, order_number=order_number, language=language,
description=description, raw_response=raw_response)
@@ -115,7 +118,7 @@ def register(config: NetaxeptConfig, amount: Money, order_number: Union[str, int
@dataclass
class ProcessResponse:
response_code: str
- raw_response: Dict[str, str]
+ raw_response: Dict[str, Any]
def process(config: NetaxeptConfig, transaction_id: str, operation: NetaxeptOperation,
@@ -138,11 +141,8 @@ def process(config: NetaxeptConfig, transaction_id: str, operation: NetaxeptOper
'transactionAmount': _decimal_to_netaxept_amount(amount),
}
- response = requests.post(url=urljoin(config.base_url, 'Netaxept/Process.aspx'),
- data=params)
-
+ response = requests.post(url=urljoin(config.base_url, 'Netaxept/Process.aspx'), data=params)
raw_response = _build_raw_response(response)
-
logger.info('netaxept-process-response', transaction_id=transaction_id, operation=operation.value,
amount=amount, raw_response=raw_response)
@@ -162,6 +162,44 @@ def get_payment_terminal_url(config: NetaxeptConfig, transaction_id: str) -> str
return '{}?{}'.format(urljoin(config.base_url, 'Terminal/default.aspx'), qs)
+@dataclass
+class QueryResponse:
+ """ The query response is a very complete object, but we just model what's interesting for our use-case.
+ (The complete response is captured in the raw_response)
+ """
+ annuled: bool
+ authorized: bool
+ raw_response: Dict[str, Any]
+
+
+def query(config: NetaxeptConfig, transaction_id: str) -> QueryResponse:
+ logger.info('netaxept-query', transaction_id=transaction_id)
+
+ params = {
+ 'merchantId': config.merchant_id,
+ 'token': config.secret,
+ 'transactionId': transaction_id,
+ }
+
+ response = requests.post(url=urljoin(config.base_url, 'Netaxept/Query.aspx'), data=params)
+ raw_response = _build_raw_response(response)
+ logger.info('netaxept-query-response', transaction_id=transaction_id, raw_response=raw_response)
+ if response.status_code == requests.codes.ok:
+ d = xmltodict.parse(response.text)
+ if 'PaymentInfo' in d:
+ summary = d['PaymentInfo']['Summary']
+ annuled = summary['Annuled'] == 'true'
+ authorized = summary['Authorized'] == 'true'
+ return QueryResponse(
+ annuled=annuled,
+ authorized=authorized,
+ raw_response=raw_response
+ )
+ elif 'Exception' in d:
+ raise NetaxeptProtocolError(d['Exception']['Error']['Message'], raw_response)
+ raise NetaxeptProtocolError(response.reason, raw_response)
+
+
def _decimal_to_netaxept_amount(decimal_amount: Decimal) -> int:
""" Return the netaxept representation of the decimal representation of the amount. """
return int((decimal_amount * 100).to_integral_value())
diff --git a/tests/gateways/test_netaxept.py b/tests/gateways/test_netaxept.py
index fde536e..92e383f 100644
--- a/tests/gateways/test_netaxept.py
+++ b/tests/gateways/test_netaxept.py
@@ -11,7 +11,7 @@
from payment.gateways.netaxept import gateway_to_netaxept_config, capture, refund
from payment.gateways.netaxept.netaxept_protocol import NetaxeptConfig, get_payment_terminal_url, \
_iso6391_to_netaxept_language, _money_to_netaxept_amount, _money_to_netaxept_currency, register, RegisterResponse, \
- NetaxeptProtocolError, process, ProcessResponse, NetaxeptOperation
+ NetaxeptProtocolError, process, ProcessResponse, NetaxeptOperation, query, QueryResponse
from payment.interface import GatewayResponse
from payment.utils import create_payment_information
@@ -79,7 +79,8 @@ def it_should_register(requests_post):
7624b99699f344e3b6da9884d20f0b27
""")
requests_post.return_value = mock_response
- register_response = register(_netaxept_config, amount=Money(10, 'CHF'), order_number='123')
+ register_response = register(config=_netaxept_config, amount=Money(10, 'CHF'), order_number='123',
+ customer_email='nwolff@gmail.com')
assert register_response == RegisterResponse(
transaction_id='7624b99699f344e3b6da9884d20f0b27',
raw_response=asdict(mock_response))
@@ -87,7 +88,7 @@ def it_should_register(requests_post):
url='https://test.epayment.nets.eu/Netaxept/Register.aspx',
data={'merchantId': '123456', 'token': 'supersekret', 'description': None, 'orderNumber': '123',
'amount': 1000, 'currencyCode': 'CHF', 'autoAuth': True, 'terminalSinglePage': True,
- 'language': None, 'redirectUrl': 'http://localhost'})
+ 'language': None, 'customerEmail': 'nwolff@gmail.com', 'redirectUrl': 'http://localhost'})
@patch('requests.post')
@@ -176,6 +177,93 @@ def it_should_handle_process_failure(requests_post):
'transactionId': '1111111111114cf693a1cf86123e0d8f', 'transactionAmount': 1000})
+@patch('requests.post')
+def it_should_query(requests_post):
+ mock_response = MockResponse(
+ status_code=200,
+ url='https://test.epayment.nets.eu/Netaxept/Query.aspx',
+ encoding='ISO-8859-1',
+ reason='OK',
+ text="""
+
+ 11111111
+ 2019-10-14T10:15:07.2677951+02:00
+ 1111111111114cf693a1cf86123e0d8f
+
+ 700
+ NOK
+ 7
+
+ 0
+ 0
+ 700
+ 2019-09-11T16:30:06.967
+
+
+ 2019-09-11T16:30:08.513
+ 2019-09-11T16:30:24.903
+ Chrome-Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36
+
+
+
+ 85.218.56.162
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 700
+ 0
+ false
+ false
+ true
+ 169337
+
+
+ Visa
+ NO
+ 492500******0004
+ Visa
+ 2301
+ 3
+
+
+
+ 2019-09-11T16:30:06.967
+ Register
+
+
+ 2019-09-11T16:30:24.81
+ 127.0.0.1: Auto AUTH
+ Auth
+ 672
+
+
+
+
+
+
+ CH
+ false
+
+ """)
+ requests_post.return_value = mock_response
+ query_response = query(config=_netaxept_config, transaction_id='233abb21f18b47dc98469fb9000b1f21')
+ assert query_response == QueryResponse(
+ annuled=False,
+ authorized=True,
+ raw_response=asdict(mock_response))
+
+
##############################################################################
# SPI tests
@@ -199,9 +287,9 @@ def it_should_capture(process, netaxept_authorized_payment):
'text': '\n \n 675\n 2019-09-16T17:31:00.7593672+02:00\n 123456\n CAPTURE\n OK\n 1111111111114cf693a1cf86123e0d8f\n '})
process.return_value = mock_process_response
payment_info = create_payment_information(
- payment=netaxept_authorized_payment,
- payment_token='1111111111114cf693a1cf86123e0d8f',
- amount=Money(10, 'CHF'))
+ payment=netaxept_authorized_payment,
+ payment_token='1111111111114cf693a1cf86123e0d8f',
+ amount=Money(10, 'CHF'))
capture_result = capture(config=_gateway_config, payment_information=payment_info)
assert capture_result == GatewayResponse(
is_success=True,
@@ -226,9 +314,9 @@ def it_should_capture(process, netaxept_authorized_payment):
def it_should_not_capture_when_protocol_error(process, netaxept_authorized_payment):
process.side_effect = NetaxeptProtocolError(error='some error', raw_response={})
payment_info = create_payment_information(
- payment=netaxept_authorized_payment,
- payment_token='1111111111114cf693a1cf86123e0d8f',
- amount=Money(10, 'CHF'))
+ payment=netaxept_authorized_payment,
+ payment_token='1111111111114cf693a1cf86123e0d8f',
+ amount=Money(10, 'CHF'))
capture_result = capture(config=_gateway_config, payment_information=payment_info)
assert capture_result == GatewayResponse(
is_success=False,
@@ -260,9 +348,9 @@ def it_should_refund(process, netaxept_authorized_payment):
'text': '\n \n 675\n 2019-09-16T17:31:00.7593672+02:00\n 123456\n REFUND\n OK\n 1111111111114cf693a1cf86123e0d8f\n '})
process.return_value = mock_process_response
payment_info = create_payment_information(
- payment=netaxept_authorized_payment,
- payment_token='1111111111114cf693a1cf86123e0d8f',
- amount=Money(10, 'CHF'))
+ payment=netaxept_authorized_payment,
+ payment_token='1111111111114cf693a1cf86123e0d8f',
+ amount=Money(10, 'CHF'))
capture_result = refund(config=_gateway_config, payment_information=payment_info)
assert capture_result == GatewayResponse(
is_success=True,