Skip to content

Commit

Permalink
query
Browse files Browse the repository at this point in the history
  • Loading branch information
nwolff committed Oct 14, 2019
1 parent 1eb0315 commit c01e9f7
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 82 deletions.
13 changes: 13 additions & 0 deletions example_project/templates/netaxept/query_result.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<html>
<body>

<p>Annuled: {{ query_response.annuled }}</p>

<p>Authorized: {{ query_response.authorized }}</p>

<p>Status code: {{ query_response.raw_response.status_code }}</p>

<p>Raw response: <pre> {{ query_response.raw_response.text }} </pre> </p>

</body>
</html>
3 changes: 3 additions & 0 deletions example_project/templates/operation_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ <H3>Operations</H3>
<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>
{% if payment.token %}
<li><a href="{% url 'netaxept_query' payment.token %}">Query</a></li>
{% endif %}
{% endif %}
<li><a href="{%url 'admin:payment_payment_change' payment.id %}">See payment in admin</a></li>
</ul>
Expand Down
40 changes: 24 additions & 16 deletions example_project/views/netaxept.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
"""
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

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()
Expand All @@ -20,7 +22,7 @@
@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.
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)

Expand All @@ -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
Expand All @@ -39,28 +41,34 @@ 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:
"""
Retries the status of the given transaction from netaxept.
"""
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/<payment_id>', register_and_authorize, name='netaxept_register_and_authorize'),
path('after_terminal', after_terminal, name='netaxept_after_terminal'),
path('query/<transaction_id>', query, name='netaxept_query'),
]
57 changes: 45 additions & 12 deletions payment/gateways/netaxept/actions.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from django.db import transaction
from django.shortcuts import get_object_or_404
from structlog import get_logger

from payment.gateways.netaxept.netaxept_protocol import query
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):
Expand All @@ -19,13 +23,15 @@ class PaymentAlreadyRegistered(NetaxeptException):
def register_payment(payment_id: int) -> str:
"""
- Registers the payment with netaxept.
- Records a Transaction representing the registration.
- Stores the newly created netaxept transaction_id in the Payment.
- Records a Transaction representing the registration.
:param payment_id: The id of a Payment object.
:return: The newly created netaxept transaction_id
:raises NetaxeptException: If the registration fails
:raises NetaxeptException: If the payment was already registered or the registration fails
"""
logger.info('netaxept-actions-register', payment_id=payment_id)

payment = get_object_or_404(Payment, id=payment_id)

if payment.token != '':
Expand All @@ -39,7 +45,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,
Expand All @@ -52,6 +59,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,
Expand All @@ -61,21 +71,44 @@ 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: The id of the transaction
:param response_code: The response code received in the callback
:return: whether the transaction was effectively authorized
"""
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 = 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 == 'OK' and not transaction_authorized:
logger.error('Inconsistent auth status detected!', transaction_id=transaction_id, response_code=response_code,
transaction_authorized=transaction_authorized)

# 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
64 changes: 51 additions & 13 deletions payment/gateways/netaxept/netaxept_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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)

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

Expand All @@ -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())
Expand Down
Loading

0 comments on commit c01e9f7

Please sign in to comment.