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 533417e
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 44 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.id %}">Query</a></li>
{% endif %}
{% endif %}
<li><a href="{%url 'admin:payment_payment_change' payment.id %}">See payment in admin</a></li>
</ul>
Expand Down
38 changes: 25 additions & 13 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 @@ -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,38 @@ 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,
Because netaxept does not include any verification means (such as digital signatures), we cannot rely on the
information received and so we need to verify if authorization was successful.
"""
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')
logger.info('netaxept-after-terminal', transaction_id=transaction_id, response_code=response_code)

actions.create_auth_transaction(transaction_id=transaction_id, success=success)
payment_authorized = actions.verify_auth_transaction(transaction_id=transaction_id, response_code=response_code)

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


def query(request: HttpRequest, payment_id: int) -> HttpResponse:
"""
Queries netaxept for the details of the transaction related to the given payment_id
"""
logger.info('netaxept-query', payment_id=payment_id)
payment = get_object_or_404(Payment, id=payment_id)
transaction_id = payment.token
payment_gateway, gateway_config = get_payment_gateway(payment.gateway)
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',
{'payment': payment, '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/<payment_id>', query, name='netaxept_query'),
]
18 changes: 12 additions & 6 deletions payment/gateways/netaxept/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,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 +53,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,16 +65,18 @@ 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, success: bool) -> bool:
"""
Record the outcome of a netaxept auth transaction.
"""
payment = Payment.objects.get(token=transaction_id)

_payment_gateway, gateway_config = get_payment_gateway('netaxept')
netaxept_config = gateway_to_netaxept_config(gateway_config)

return Transaction.objects.create(
payment=payment,
kind=TransactionKind.AUTH,
Expand Down
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 533417e

Please sign in to comment.