Skip to content

Commit

Permalink
Add a Callback object for easy callback parsing
Browse files Browse the repository at this point in the history
As described in both #20, #21 and #22, it is annoying to parse the
callbacks. This adds the callback object, which can be used to easily
parse the callback, and also map the underlying transactions in
Transaction objects.
  • Loading branch information
HenrikOssipoff committed Jan 14, 2016
1 parent 3f01848 commit 7086e76
Show file tree
Hide file tree
Showing 9 changed files with 335 additions and 1 deletion.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
Change Log
----------

0.1.dev2 (2015-01-11)
0.1.dev2 (2015-01-14)
+++++++++++++++++++++

**Features**

- Added ``altapay.Transaction`` and the ability to find a transaction by its transaction ID in the AltaPay service
- Added ``altapay.Transaction.capture()`` which captures a transaction that has already been loaded. Optinally, parameters can be passed which allows for partial captures (see the AltaPay documentation for full list of possible arguments)
- Added a public facing API for converting an AltaPay XML response (as a string) to a Python dictionary (``altapay.utils.xml_to_dict``)
- Added ``altapay.Callback`` which wraps a callback response from AltaPay, and automatically wraps the coupled transactions in ``altapay.Transaction`` objects

**Bugfixes**

Expand Down
1 change: 1 addition & 0 deletions altapay/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
}

from .api import API # NOQA
from .callback import Callback # NOQA
from .payment import Payment # NOQA
from .resource import Resource # NOQA
from .transaction import Transaction # NOQA
50 changes: 50 additions & 0 deletions altapay/callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import absolute_import, unicode_literals

from xml.etree import ElementTree

from . import utils
from .resource import Resource
from .transaction import Transaction


class Callback(Resource):
def transactions(self, auth_type=''):
"""
List all of the transactions returned by the callback.
:param auth_type: the authentication type you wish to filter.
Defaults to empty string, which means no filter will be made.
:rtype: List of :py:class:`altapay.Transaction` objects.
"""
data = self.__data__['transactions']['transaction']

if not isinstance(data, list):
data = [data]

transaction_set = data
if auth_type:
transaction_set = [
transaction for transaction in data
if transaction['AuthType'] == auth_type
]

return [
Transaction(self.version, self.__header__, transaction)
for transaction in transaction_set]

@classmethod
def from_xml_callback(cls, callback):
"""
Instantiate a :py:class:`altapay.Callback` object from an XML response.
:rtype: :py:class:`altapay.Callback` instance.
"""
if not isinstance(callback, ElementTree.Element):
callback = ElementTree.XML(callback)

response = utils.etree_to_dict(callback)['APIResponse']

return cls(
response['@version'], response['Header'],
response['Body'])
10 changes: 10 additions & 0 deletions docs/api/callback.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. _api-callback:

Callback
========

.. py:module:: altapay
.. autoclass:: Callback
:show-inheritance:
:members:
45 changes: 45 additions & 0 deletions docs/guide/callback_handling.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
.. _guide-callback-handling:

Callback Handling
=================

To make it easy to parse callbacks received from AltaPay, the special :py:class:`altapay.Callback` class can be used. Note that when you receive callbacks from AltaPay, it comes as an HTTP POST. Within this is a field called :samp:`xml`, and this is the response you should use for the :py:class:`altapay.Callback` class.

Given a callback response in the variable :samp:`xml`, this is how a callback instance can be instantiated:

.. code :: python
from altapay import Callback
xml = '' # XML response here
callback = Callback.from_xml_callback(xml)
if callback.result == 'Success':
for transaction in callback.transactions():
print(transaction)
else:
raise Exception('Callback not successful')
:py:func:`altapay.Callback.transactions` will contain a list of :py:class:`altapay.Transaction` objects. Note that even if there is only one transaction in the callback, you will have a list of just one :py:class:`altapay.Transaction`.

Using :py:func:`altapay.Callback.transactions` it is possible to filter based on a authentication type (this will depend on what you chose for the payment type). This can be useful if you have chosen to make a subscription and reservation in the same payment; in this case you can receive a callback with two transactions, and in some cases you might want to process a specific of them ahead of the other one.

This example extends the usecase described above, and filters out the subscription portion of the payment:

.. code :: python
from altapay import Callback
xml = '' # XML response here
callback = Callback.from_xml_callback(xml)
if callback.result == 'Success':
transactions = callback.transactions(auth_type=subscription_payment)
for transaction in transactions:
# Will only show transactions of the authentication type
# subscription_payment
print(transaction)
else:
raise Exception('Callback not successful')
2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Guide

guide/introduction
guide/create_payment
guide/callback_handling
guide/transactions


Expand All @@ -38,6 +39,7 @@ API Documentation
api/api
api/resource
api/payment
api/callback
api/transaction
api/exceptions
api/utils
Expand Down
55 changes: 55 additions & 0 deletions tests/test_callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import absolute_import, unicode_literals

from xml.etree import ElementTree

import responses
from altapay import API, Callback, Transaction

from .test_cases import TestCase


class PaymentTest(TestCase):
def setUp(self):
self.api = API(mode='test', auto_login=False)
self.response_as_str = self.load_xml_response(
'200_callback_multiple.xml')
self.response_as_etree = ElementTree.XML(self.response_as_str)
self.response_single_as_str = self.load_xml_response(
'200_callback_single.xml')

@responses.activate
def test_load_from_str(self):
callback = Callback.from_xml_callback(self.response_as_str)

self.assertEqual(callback.result, 'Success')

def test_load_from_etree(self):
callback = Callback.from_xml_callback(self.response_as_etree)

self.assertEqual(callback.result, 'Success')

def test_transaction_set(self):
callback = Callback.from_xml_callback(self.response_as_etree)

transactions = callback.transactions()

self.assertEqual(len(transactions), 2)
for transaction in transactions:
self.assertIsInstance(transaction, Transaction)

def test_transaction_set_filter(self):
callback = Callback.from_xml_callback(self.response_as_etree)

transactions = callback.transactions(auth_type='subscription_payment')

self.assertEqual(len(transactions), 1)
self.assertIsInstance(transactions[0], Transaction)

def test_transaction_set_single(self):
callback = Callback.from_xml_callback(self.response_single_as_str)

transactions = callback.transactions()

self.assertIsInstance(transactions, list)
self.assertEqual(len(transactions), 1)
self.assertIsInstance(transactions[0], Transaction)
109 changes: 109 additions & 0 deletions tests/xml/200_callback_multiple.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?xml version="1.0"?>
<APIResponse version="20151228">
<Header>
<Date>2016-01-12T11:13:11+01:00</Date>
<Path>API/reservationOfFixedAmount?pid=c80919f5-0b6d-4679-872a-980162e9cbf1</Path>
<ErrorCode>0</ErrorCode>
<ErrorMessage/>
</Header>
<Body>
<Result>Success</Result>
<CardHolderMessageMustBeShown>false</CardHolderMessageMustBeShown>
<Transactions>
<Transaction>
<TransactionId>8140142</TransactionId>
<PaymentId>d4a66206-881e-4d1c-b41a-55e27828ab70</PaymentId>
<AuthType>subscriptionAndReserve</AuthType>
<CardStatus>InvalidLuhn</CardStatus>
<CreditCardExpiry>
<Year>2016</Year>
<Month>01</Month>
</CreditCardExpiry>
<CreditCardToken>e48a3bb37ab9e2aeb935b42a3c37d1dd42be768b</CreditCardToken>
<CreditCardMaskedPan>500000******0000</CreditCardMaskedPan>
<ThreeDSecureResult>Not_Attempted</ThreeDSecureResult>
<LiableForChargeback>Merchant</LiableForChargeback>
<CVVCheckResult>Not_Attempted</CVVCheckResult>
<BlacklistToken>cf9d894c143b3448697726523d3fec4ab73d2da3</BlacklistToken>
<ShopOrderId>8000011</ShopOrderId>
<Shop>Test Shop</Shop>
<Terminal>Test Terminal</Terminal>
<TransactionStatus>recurring_confirmed</TransactionStatus>
<ReasonCode>NONE</ReasonCode>
<MerchantCurrency>208</MerchantCurrency>
<MerchantCurrencyAlpha>DKK</MerchantCurrencyAlpha>
<CardHolderCurrency>208</CardHolderCurrency>
<CardHolderCurrencyAlpha>DKK</CardHolderCurrencyAlpha>
<ReservedAmount>13.29</ReservedAmount>
<CapturedAmount>0.00</CapturedAmount>
<RefundedAmount>0.00</RefundedAmount>
<CreditedAmount>0.00</CreditedAmount>
<RecurringDefaultAmount>13.29</RecurringDefaultAmount>
<SurchargeAmount>0.00</SurchargeAmount>
<CreatedDate>2016-01-12 11:13:10</CreatedDate>
<UpdatedDate>2016-01-12 11:13:11</UpdatedDate>
<PaymentNature>CreditCard</PaymentNature>
<PaymentSchemeName>Maestro</PaymentSchemeName>
<PaymentNatureService name="SoapTestAcquirer">
<SupportsRefunds>true</SupportsRefunds>
<SupportsRelease>true</SupportsRelease>
<SupportsMultipleCaptures>true</SupportsMultipleCaptures>
<SupportsMultipleRefunds>true</SupportsMultipleRefunds>
</PaymentNatureService>
<AddressVerification>L</AddressVerification>
<AddressVerificationDescription>Invalid address verification response code</AddressVerificationDescription>
<ChargebackEvents/>
<PaymentInfos>
<PaymentInfo name="0"><![CDATA[COOL]]></PaymentInfo>
</PaymentInfos>
<ReconciliationIdentifiers/>
</Transaction>
<Transaction>
<TransactionId>8140152</TransactionId>
<PaymentId>d851583c-bdbd-489b-a237-bc153916028c</PaymentId>
<AuthType>subscription_payment</AuthType>
<CardStatus>InvalidLuhn</CardStatus>
<CreditCardExpiry>
<Year>2016</Year>
<Month>01</Month>
</CreditCardExpiry>
<CreditCardToken>e48a3bb37ab9e2aeb935b42a3c37d1dd42be768b</CreditCardToken>
<CreditCardMaskedPan>500000******0000</CreditCardMaskedPan>
<ThreeDSecureResult>Not_Applicable</ThreeDSecureResult>
<LiableForChargeback>Merchant</LiableForChargeback>
<CVVCheckResult>Not_Applicable</CVVCheckResult>
<BlacklistToken>cf9d894c143b3448697726523d3fec4ab73d2da3</BlacklistToken>
<ShopOrderId>8000011</ShopOrderId>
<Shop>Test Shop</Shop>
<Terminal>Test Terminal</Terminal>
<TransactionStatus>preauth</TransactionStatus>
<ReasonCode>NONE</ReasonCode>
<MerchantCurrency>208</MerchantCurrency>
<MerchantCurrencyAlpha>DKK</MerchantCurrencyAlpha>
<CardHolderCurrency>208</CardHolderCurrency>
<CardHolderCurrencyAlpha>DKK</CardHolderCurrencyAlpha>
<ReservedAmount>13.29</ReservedAmount>
<CapturedAmount>0.00</CapturedAmount>
<RefundedAmount>0.00</RefundedAmount>
<CreditedAmount>0.00</CreditedAmount>
<RecurringDefaultAmount>0.00</RecurringDefaultAmount>
<SurchargeAmount>0.00</SurchargeAmount>
<CreatedDate>2016-01-12 11:13:11</CreatedDate>
<UpdatedDate>2016-01-12 11:13:11</UpdatedDate>
<PaymentNature>CreditCard</PaymentNature>
<PaymentSchemeName>Maestro</PaymentSchemeName>
<PaymentNatureService name="SoapTestAcquirer">
<SupportsRefunds>true</SupportsRefunds>
<SupportsRelease>true</SupportsRelease>
<SupportsMultipleCaptures>true</SupportsMultipleCaptures>
<SupportsMultipleRefunds>true</SupportsMultipleRefunds>
</PaymentNatureService>
<AddressVerification>L</AddressVerification>
<AddressVerificationDescription>Invalid address verification response code</AddressVerificationDescription>
<ChargebackEvents/>
<PaymentInfos/>
<ReconciliationIdentifiers/>
</Transaction>
</Transactions>
</Body>
</APIResponse>
61 changes: 61 additions & 0 deletions tests/xml/200_callback_single.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?xml version="1.0"?>
<APIResponse version="20151228">
<Header>
<Date>2016-01-12T11:13:11+01:00</Date>
<Path>API/reservationOfFixedAmount?pid=c80919f5-0b6d-4679-872a-980162e9cbf1</Path>
<ErrorCode>0</ErrorCode>
<ErrorMessage/>
</Header>
<Body>
<Result>Success</Result>
<CardHolderMessageMustBeShown>false</CardHolderMessageMustBeShown>
<Transactions>
<Transaction>
<TransactionId>8140152</TransactionId>
<PaymentId>d851583c-bdbd-489b-a237-bc153916028c</PaymentId>
<AuthType>subscription_payment</AuthType>
<CardStatus>InvalidLuhn</CardStatus>
<CreditCardExpiry>
<Year>2016</Year>
<Month>01</Month>
</CreditCardExpiry>
<CreditCardToken>e48a3bb37ab9e2aeb935b42a3c37d1dd42be768b</CreditCardToken>
<CreditCardMaskedPan>500000******0000</CreditCardMaskedPan>
<ThreeDSecureResult>Not_Applicable</ThreeDSecureResult>
<LiableForChargeback>Merchant</LiableForChargeback>
<CVVCheckResult>Not_Applicable</CVVCheckResult>
<BlacklistToken>cf9d894c143b3448697726523d3fec4ab73d2da3</BlacklistToken>
<ShopOrderId>8000011</ShopOrderId>
<Shop>Test Shop</Shop>
<Terminal>Test Terminal</Terminal>
<TransactionStatus>preauth</TransactionStatus>
<ReasonCode>NONE</ReasonCode>
<MerchantCurrency>208</MerchantCurrency>
<MerchantCurrencyAlpha>DKK</MerchantCurrencyAlpha>
<CardHolderCurrency>208</CardHolderCurrency>
<CardHolderCurrencyAlpha>DKK</CardHolderCurrencyAlpha>
<ReservedAmount>13.29</ReservedAmount>
<CapturedAmount>0.00</CapturedAmount>
<RefundedAmount>0.00</RefundedAmount>
<CreditedAmount>0.00</CreditedAmount>
<RecurringDefaultAmount>0.00</RecurringDefaultAmount>
<SurchargeAmount>0.00</SurchargeAmount>
<CreatedDate>2016-01-12 11:13:11</CreatedDate>
<UpdatedDate>2016-01-12 11:13:11</UpdatedDate>
<PaymentNature>CreditCard</PaymentNature>
<PaymentSchemeName>Maestro</PaymentSchemeName>
<PaymentNatureService name="SoapTestAcquirer">
<SupportsRefunds>true</SupportsRefunds>
<SupportsRelease>true</SupportsRelease>
<SupportsMultipleCaptures>true</SupportsMultipleCaptures>
<SupportsMultipleRefunds>true</SupportsMultipleRefunds>
</PaymentNatureService>
<AddressVerification>L</AddressVerification>
<AddressVerificationDescription>Invalid address verification response code</AddressVerificationDescription>
<ChargebackEvents/>
<PaymentInfos/>
<ReconciliationIdentifiers/>
</Transaction>
</Transactions>
</Body>
</APIResponse>

0 comments on commit 7086e76

Please sign in to comment.