├── requirements.txt
├── authorize
├── apis
│ ├── __init__.py
│ ├── base_api.py
│ ├── bank_account_api.py
│ ├── batch_api.py
│ ├── payment_profile_api.py
│ ├── credit_card_api.py
│ ├── address_api.py
│ ├── authorize_api.py
│ ├── customer_api.py
│ ├── recurring_api.py
│ └── transaction_api.py
├── environment.py
├── batch.py
├── address.py
├── bank_account.py
├── configuration.py
├── recurring.py
├── credit_card.py
├── exceptions.py
├── customer.py
├── __init__.py
├── transaction.py
├── response_parser.py
└── xml_data.py
├── .gitignore
├── .travis.yml
├── tests
├── __init__.py
├── test_live_batch.py
├── test_live_address.py
├── test_batch_api.py
├── test_live_bank_account.py
├── test_live_customer.py
├── test_live_credit_card.py
├── test_address_api.py
├── test_schemas.py
├── test_live_recurring.py
├── test_bank_account_api.py
├── test_xml_data.py
├── test_customer_api.py
├── test_credit_card_api.py
├── test_response_parser.py
└── test_recurring_api.py
├── tox.ini
├── docs
├── _templates
│ └── page.html
├── batch.rst
├── getting_started.rst
├── install.rst
├── index.rst
├── advanced.rst
├── development.rst
├── address.rst
├── pay_pal.rst
├── customer.rst
├── credit_card.rst
├── bank_account.rst
├── recurring.rst
├── conf.py
└── transaction.rst
├── LICENSE
├── setup.py
├── README.rst
├── make.bat
└── Makefile
/requirements.txt:
--------------------------------------------------------------------------------
1 | colander>=1.0b1
2 |
--------------------------------------------------------------------------------
/authorize/apis/__init__.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | *.py[co]
3 | .DS_Store
4 | docs/_build/
5 | build/
6 | _build/
7 | dist/
8 | *.egg-info/
9 |
10 | .idea
11 | .tox
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - 2.7
4 | - 3.5
5 | - 3.6
6 |
7 | install:
8 | - pip install -r requirements.txt
9 |
10 | script:
11 | - nosetests
12 |
--------------------------------------------------------------------------------
/authorize/environment.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class Environment(object):
4 | TEST = 'https://apitest.authorize.net/xml/v1/request.api'
5 | PRODUCTION = 'https://api2.authorize.net/xml/v1/request.api'
6 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from authorize.configuration import Configuration
2 | from authorize.environment import Environment
3 |
4 |
5 | def setUpPackage():
6 | Configuration.configure(
7 | Environment.TEST,
8 | '8s8tVnG5t',
9 | '5GK7mncw8mG2946z',
10 | )
11 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py27,py33,py34,py35,pypy
3 |
4 | [testenv]
5 | deps =
6 | colander>=1.0b1
7 | nose
8 | commands = nosetests -a '!live_tests'
9 |
10 | [testenv:live]
11 | basepython = python2.7
12 | commands = nosetests
13 | deps =
14 | colander>=1.0b1
15 | nose
16 |
--------------------------------------------------------------------------------
/authorize/batch.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 |
3 |
4 | class Batch(object):
5 |
6 | @staticmethod
7 | def details(batch_id):
8 | return Configuration.api.batch.details(batch_id)
9 |
10 | @staticmethod
11 | def list(params={}):
12 | return Configuration.api.batch.list(params)
13 |
--------------------------------------------------------------------------------
/authorize/apis/base_api.py:
--------------------------------------------------------------------------------
1 | import colander
2 |
3 | from authorize.exceptions import AuthorizeInvalidError
4 |
5 |
6 | class BaseAPI(object):
7 |
8 | def __init__(self, api):
9 | self.api = api
10 | self.config = api.config
11 |
12 | def _deserialize(self, schema, params={}):
13 | try:
14 | deserialized = schema.deserialize(params)
15 | except colander.Invalid as e:
16 | raise AuthorizeInvalidError(e)
17 | return deserialized
18 |
--------------------------------------------------------------------------------
/docs/_templates/page.html:
--------------------------------------------------------------------------------
1 | {% extends "!page.html" %}
2 | {% block footer %}
3 | {{ super() }}
4 |
13 | {% endblock %}
--------------------------------------------------------------------------------
/authorize/address.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 |
3 |
4 | class Address(object):
5 |
6 | @staticmethod
7 | def create(customer_id, params={}):
8 | return Configuration.api.address.create(customer_id, params)
9 |
10 | @staticmethod
11 | def details(customer_id, address_id):
12 | return Configuration.api.address.details(customer_id, address_id)
13 |
14 | @staticmethod
15 | def update(customer_id, address_id, params={}):
16 | return Configuration.api.address.update(customer_id, address_id, params)
17 |
18 | @staticmethod
19 | def delete(customer_id, address_id):
20 | return Configuration.api.address.delete(customer_id, address_id)
21 |
--------------------------------------------------------------------------------
/authorize/bank_account.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 |
3 |
4 | class BankAccount(object):
5 |
6 | @staticmethod
7 | def create(customer_id, params={}):
8 | return Configuration.api.bank_account.create(customer_id, params)
9 |
10 | @staticmethod
11 | def details(customer_id, payment_id):
12 | return Configuration.api.bank_account.details(customer_id, payment_id)
13 |
14 | @staticmethod
15 | def update(customer_id, payment_id, params={}):
16 | return Configuration.api.bank_account.update(customer_id, payment_id, params)
17 |
18 | @staticmethod
19 | def delete(customer_id, payment_id):
20 | return Configuration.api.bank_account.delete(customer_id, payment_id)
21 |
--------------------------------------------------------------------------------
/authorize/configuration.py:
--------------------------------------------------------------------------------
1 | from authorize.apis.authorize_api import AuthorizeAPI
2 |
3 |
4 | class Configuration(object):
5 |
6 | @staticmethod
7 | def configure(environment, login_id, transaction_key):
8 | Configuration.environment = environment
9 | Configuration.login_id = login_id
10 | Configuration.transaction_key = transaction_key
11 | Configuration.api = AuthorizeAPI(Configuration.instantiate())
12 |
13 | @staticmethod
14 | def instantiate():
15 | return Configuration(
16 | Configuration.environment,
17 | Configuration.login_id,
18 | Configuration.transaction_key,
19 | )
20 |
21 | def __init__(self, environment, login_id, transaction_key):
22 | self.environment = environment
23 | self.login_id = login_id
24 | self.transaction_key = transaction_key
25 |
--------------------------------------------------------------------------------
/authorize/recurring.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 |
3 |
4 | class Recurring(object):
5 |
6 | @staticmethod
7 | def create(params={}):
8 | return Configuration.api.recurring.create(params)
9 |
10 | @staticmethod
11 | def details(subscription_id):
12 | return Configuration.api.recurring.details(subscription_id)
13 |
14 | @staticmethod
15 | def status(subscription_id):
16 | return Configuration.api.recurring.status(subscription_id)
17 |
18 | @staticmethod
19 | def update(subscription_id, params={}):
20 | return Configuration.api.recurring.update(subscription_id, params)
21 |
22 | @staticmethod
23 | def delete(subscription_id):
24 | return Configuration.api.recurring.delete(subscription_id)
25 |
26 | @staticmethod
27 | def list(params={}):
28 | return Configuration.api.recurring.list(params)
29 |
--------------------------------------------------------------------------------
/authorize/credit_card.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 |
3 |
4 | class CreditCard(object):
5 |
6 | @staticmethod
7 | def create(customer_id, params={}):
8 | return Configuration.api.credit_card.create(customer_id, params)
9 |
10 | @staticmethod
11 | def details(customer_id, payment_id):
12 | return Configuration.api.credit_card.details(customer_id, payment_id)
13 |
14 | @staticmethod
15 | def update(customer_id, payment_id, params={}):
16 | return Configuration.api.credit_card.update(customer_id, payment_id, params)
17 |
18 | @staticmethod
19 | def delete(customer_id, payment_id):
20 | return Configuration.api.credit_card.delete(customer_id, payment_id)
21 |
22 | @staticmethod
23 | def validate(customer_id, payment_id, params={}):
24 | return Configuration.api.credit_card.validate(customer_id, payment_id, params)
25 |
--------------------------------------------------------------------------------
/authorize/exceptions.py:
--------------------------------------------------------------------------------
1 | from colander import Invalid
2 |
3 |
4 | class AuthorizeError(Exception):
5 |
6 | """Base class for connection and response errors."""
7 |
8 |
9 | class AuthorizeConnectionError(AuthorizeError):
10 |
11 | """Error communicating with the Authorize.net API."""
12 |
13 |
14 | class AuthorizeResponseError(AuthorizeError):
15 |
16 | """Error response code returned from API."""
17 |
18 | def __init__(self, code, text, full_response):
19 | self.code = code
20 | self.text = text
21 | self.full_response = full_response
22 |
23 | def __str__(self):
24 | return '%s: %s' % (self.code, self.text)
25 |
26 |
27 | class AuthorizeInvalidError(AuthorizeError, Invalid):
28 |
29 | def __init__(self, invalid):
30 | self.node = invalid.node
31 | self.msg = invalid.msg
32 | self.value = invalid.value
33 | self.children = invalid.children
34 |
--------------------------------------------------------------------------------
/docs/batch.rst:
--------------------------------------------------------------------------------
1 | Batch
2 | =====
3 |
4 | Transactions are batched and sent for settlement on a daily basis.
5 | Py-Authorize provides basic batch methods based on Authorize.net's
6 | reporting API.
7 |
8 | List
9 | ----
10 |
11 | The `list` method returns the batch ID, Settlement Time and other batch
12 | statistics for all settled batches within a range of dates.
13 |
14 | .. code-block:: python
15 |
16 | result = authorize.Batch.list({
17 | 'start': '2012-01-01',
18 | 'end': '2012-05-31',
19 | })
20 |
21 |
22 | If the `start` and `end` dates are not specified, the `list` method will
23 | return the batches processed in the past 24 hours.
24 |
25 | .. code-block:: python
26 |
27 | result = authorize.Batch.list()
28 |
29 |
30 | Details
31 | -------
32 |
33 | The `details` method returns batch statistics for a given batch ID.
34 |
35 | .. code-block:: python
36 |
37 | result = authorize.Batch.details('2552096')
38 |
39 |
--------------------------------------------------------------------------------
/authorize/customer.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 |
3 |
4 | class Customer(object):
5 |
6 | @staticmethod
7 | def create(params={}):
8 | return Configuration.api.customer.create(params)
9 |
10 | @staticmethod
11 | def from_transaction(transaction_id, params={}):
12 | return Configuration.api.customer.from_transaction(transaction_id, params)
13 |
14 | @staticmethod
15 | def details(customer_id):
16 | return Configuration.api.customer.details(customer_id)
17 |
18 | @staticmethod
19 | def update(customer_id, params={}):
20 | return Configuration.api.customer.update(customer_id, params)
21 |
22 | @staticmethod
23 | def delete(customer_id):
24 | return Configuration.api.customer.delete(customer_id)
25 |
26 | @staticmethod
27 | def list():
28 | return Configuration.api.customer.list()
29 |
30 | @staticmethod
31 | def get_transactions(customer_id, payment_id=None):
32 | return Configuration.api.customer.get_transactions(customer_id, payment_id)
33 |
--------------------------------------------------------------------------------
/authorize/__init__.py:
--------------------------------------------------------------------------------
1 | import xml.etree.ElementTree as E
2 |
3 | from authorize.configuration import Configuration
4 | from authorize.address import Address
5 | from authorize.bank_account import BankAccount
6 | from authorize.batch import Batch
7 | from authorize.credit_card import CreditCard
8 | from authorize.customer import Customer
9 | from authorize.environment import Environment
10 | from authorize.exceptions import AuthorizeError
11 | from authorize.exceptions import AuthorizeConnectionError
12 | from authorize.exceptions import AuthorizeResponseError
13 | from authorize.exceptions import AuthorizeInvalidError
14 | from authorize.recurring import Recurring
15 | from authorize.transaction import Transaction
16 |
17 |
18 | # Monkeypatch the ElementTree module so that we can use CDATA element types
19 | E._original_serialize_xml = E._serialize_xml
20 | def _serialize_xml(write, elem, *args, **kwargs):
21 | if elem.tag == '![CDATA[':
22 | write('' % elem.text)
23 | return
24 | return E._original_serialize_xml(write, elem, *args, **kwargs)
25 | E._serialize_xml = E._serialize['xml'] = _serialize_xml
26 |
--------------------------------------------------------------------------------
/tests/test_live_batch.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from authorize import Batch
4 | from authorize import AuthorizeResponseError
5 |
6 | from nose.plugins.attrib import attr
7 |
8 | from unittest import TestCase
9 |
10 | LIST_BATCH_DATES = {
11 | 'start': datetime.datetime(datetime.date.today().year - 1, 5, 1).strftime("%Y-%m-%d"), #'2012-05-01'
12 | 'end': datetime.datetime(datetime.date.today().year - 1, 5, 31).strftime("%Y-%m-%d"), #'2012-05-31'
13 | }
14 |
15 | LIST_BATCH_DATES_START_ONLY = {
16 | 'start': datetime.datetime(datetime.date.today().year - 1, 1, 1).strftime("%Y-%m-%d"), #'2012-01-01'
17 | }
18 |
19 |
20 | @attr('live_tests')
21 | class BatchTests(TestCase):
22 |
23 | def test_batch_details(self):
24 | Batch.details('2520288')
25 | self.assertRaises(AuthorizeResponseError, Batch.details, 'Bad batch ID')
26 |
27 | def test_list_batch(self):
28 | Batch.list()
29 | Batch.list(LIST_BATCH_DATES)
30 | # Both start and end dates are required by the gateway when one is
31 | # provided
32 | self.assertRaises(AuthorizeResponseError, Batch.list, LIST_BATCH_DATES_START_ONLY)
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Vincent Catalano
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/authorize/apis/bank_account_api.py:
--------------------------------------------------------------------------------
1 | from authorize.apis.payment_profile_api import PaymentProfileAPI
2 | from authorize.schemas import CreateBankAccountSchema
3 | from authorize.xml_data import *
4 |
5 |
6 | class BankAccountAPI(PaymentProfileAPI):
7 |
8 | def create(self, customer_id, params={}):
9 | card = self._deserialize(CreateBankAccountSchema(), params)
10 | return self.api._make_call(self._create_request(customer_id, card))
11 |
12 | def update(self, customer_id, payment_id, params={}):
13 | card = self._deserialize(CreateBankAccountSchema(), params)
14 | return self.api._make_call(self._update_request(customer_id, payment_id, card))
15 |
16 | # The following methods generate the XML for the corresponding API calls.
17 | # This makes unit testing each of the calls easier.
18 | def _create_request(self, customer_id, card={}):
19 | return self._make_xml('createCustomerPaymentProfileRequest', customer_id, None, params=card)
20 |
21 | def _update_request(self, customer_id, payment_id, card={}):
22 | return self._make_xml('updateCustomerPaymentProfileRequest', customer_id, payment_id, params=card)
23 |
--------------------------------------------------------------------------------
/docs/getting_started.rst:
--------------------------------------------------------------------------------
1 | Getting Started
2 | ===============
3 |
4 | The first step when using the Py-Authorize API is to initialize the client
5 | with your Authorize.net API login name and transaction key. The
6 | initialization will only need to occur once in your application and must be
7 | setup before any other API calls are used.
8 |
9 | Test Environment
10 | ~~~~~~~~~~~~~~~~
11 |
12 | .. code-block:: python
13 |
14 | import authorize
15 |
16 | authorize.Configuration.configure(
17 | authorize.Environment.TEST,
18 | 'api_login_id',
19 | 'api_transaction_key',
20 | )
21 |
22 | In addition to the Authorize.net API login name and transaction key, the
23 | ``configure`` method also takes an ``Environment`` parameter. For development
24 | and testing configurations users should use the ``Environment.TEST``
25 | variable. For production configurations, users should use the
26 | ``Environment.PRODUCTION`` variable:
27 |
28 | Production Environment
29 | ~~~~~~~~~~~~~~~~~~~~~~
30 |
31 | .. code-block:: python
32 |
33 | import authorize
34 |
35 | authorize.Configuration.configure(
36 | authorize.Environment.PRODUCTION,
37 | 'api_login_id',
38 | 'api_transaction_key',
39 | )
40 |
41 |
--------------------------------------------------------------------------------
/docs/install.rst:
--------------------------------------------------------------------------------
1 | Installation
2 | ============
3 |
4 | Install with pip
5 | ----------------
6 |
7 | If you are using pip_, you can install the :app:'Py-Authorize' package using the
8 | following commands:
9 |
10 | .. code-block:: text
11 |
12 | pip install py-authorize
13 |
14 | .. _pip: http://www.pip-installer.org/
15 |
16 |
17 | Install with virtualenv
18 | -----------------------
19 |
20 | If you are using virtualenv_ to manage your packages, you can install
21 | :app:'Py-Authorize' using the following commands:
22 |
23 | .. code-block:: text
24 |
25 | easy_install py-authorize
26 |
27 | .. _virtualenv: http://www.virtualenv.org/
28 |
29 |
30 | Install from source
31 | -------------------
32 |
33 | Download or clone the source from Github and run setup.py install:
34 |
35 | .. code-block:: text
36 |
37 | git clone http://github.com/vcatalano/py-authorize.git
38 | cd py-authorize
39 | python setup.py install
40 |
41 |
42 | Requirements
43 | ------------
44 |
45 | Py-Authorize has only one external dependency:
46 |
47 | * colander_
48 |
49 | If you want to build the docs or run the tests, there are additional
50 | dependencies, which are covered in the :doc:`development` section.
51 |
52 | .. _colander: http://docs.pylonsproject.org/projects/colander
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 |
4 | setup(
5 | name='Py-Authorize',
6 | version='1.4.0.0',
7 | author='Vincent Catalano',
8 | author_email='vincent@vincentcatlano.com',
9 | url='https://github.com/vcatalano/py-authorize',
10 | download_url='',
11 | description="A full-featured Python API for Authorize.net's AIM, CIM, ARB and Reporting APIs.",
12 | long_description=__doc__,
13 | license='MIT',
14 | install_requires=[
15 | 'colander>=1.0b1',
16 | ],
17 | packages=[
18 | 'authorize',
19 | 'authorize.apis',
20 | ],
21 | classifiers=[
22 | 'Environment :: Console',
23 | 'Environment :: Web Environment',
24 | 'Intended Audience :: Developers',
25 | 'Programming Language :: Python :: 2.7',
26 | 'Programming Language :: Python :: 3',
27 | 'Programming Language :: Python :: 3.2',
28 | 'Programming Language :: Python :: 3.3',
29 | 'Programming Language :: Python :: 3.4',
30 | 'Programming Language :: Python :: 3.5',
31 | 'Programming Language :: Python :: Implementation :: PyPy',
32 | 'License :: OSI Approved :: MIT License',
33 | 'Topic :: Office/Business :: Financial',
34 | 'Topic :: Internet :: WWW/HTTP',
35 | ],
36 | )
37 |
--------------------------------------------------------------------------------
/authorize/apis/batch_api.py:
--------------------------------------------------------------------------------
1 | import xml.etree.cElementTree as E
2 |
3 | from authorize.apis.base_api import BaseAPI
4 | from authorize.schemas import ListBatchSchema
5 | from authorize.xml_data import *
6 |
7 |
8 | class BatchAPI(BaseAPI):
9 |
10 | def details(self, batch_id):
11 | return self.api._make_call(self._details_request(batch_id))
12 |
13 | def list(self, params={}):
14 | batch = self._deserialize(ListBatchSchema(), params)
15 | return self.api._make_call(self._list_request(batch))
16 |
17 | # The following methods generate the XML for the corresponding API calls.
18 | # This makes unit testing each of the calls easier.
19 | def _details_request(self, batch_id):
20 | request = self.api._base_request('getBatchStatisticsRequest')
21 | E.SubElement(request, 'batchId').text = batch_id
22 | return request
23 |
24 | def _list_request(self, params={}):
25 | request = self.api._base_request('getSettledBatchListRequest')
26 | E.SubElement(request, 'includeStatistics').text = 'true'
27 | if 'start' in params:
28 | E.SubElement(request, 'firstSettlementDate').text = params['start'].strftime('%Y-%m-%dT%H:%M:%S')
29 | if 'end' in params:
30 | E.SubElement(request, 'lastSettlementDate').text = params['end'].strftime('%Y-%m-%dT%H:%M:%S')
31 | return request
32 |
--------------------------------------------------------------------------------
/authorize/transaction.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 |
3 |
4 | class Transaction(object):
5 |
6 | @staticmethod
7 | def sale(params={}):
8 | return Configuration.api.transaction.sale(params)
9 |
10 | @staticmethod
11 | def auth(params={}):
12 | return Configuration.api.transaction.auth(params)
13 |
14 | @staticmethod
15 | def settle(transaction_id, amount=None):
16 | return Configuration.api.transaction.settle(transaction_id, amount)
17 |
18 | @staticmethod
19 | def credit(params={}):
20 | return Configuration.api.transaction.credit(params)
21 |
22 | @staticmethod
23 | def auth_continue(params={}):
24 | return Configuration.api.transaction.auth_continue(params)
25 |
26 | @staticmethod
27 | def sale_continue(params={}):
28 | return Configuration.api.transaction.sale_continue(params)
29 |
30 | @staticmethod
31 | def refund(params={}):
32 | return Configuration.api.transaction.refund(params)
33 |
34 | @staticmethod
35 | def void(transaction_id):
36 | return Configuration.api.transaction.void(transaction_id)
37 |
38 | @staticmethod
39 | def details(transaction_id):
40 | return Configuration.api.transaction.details(transaction_id)
41 |
42 | @staticmethod
43 | def list(batch_id=None):
44 | return Configuration.api.transaction.list(batch_id)
45 |
--------------------------------------------------------------------------------
/tests/test_live_address.py:
--------------------------------------------------------------------------------
1 | from authorize import Address
2 | from authorize import Customer
3 | from authorize import AuthorizeResponseError
4 |
5 | from nose.plugins.attrib import attr
6 |
7 | from unittest import TestCase
8 |
9 | ADDRESS = {
10 | 'first_name': 'Rob',
11 | 'last_name': 'Oteron',
12 | 'company': 'Robotron Studios',
13 | 'address': '101 Computer Street',
14 | 'city': 'Tucson',
15 | 'state': 'AZ',
16 | 'zip': '85704',
17 | 'country': 'US',
18 | 'phone_number': '520-123-4567',
19 | 'fax_number': '520-456-7890',
20 | }
21 |
22 |
23 | @attr('live_tests')
24 | class AddressTests(TestCase):
25 |
26 | def test_live_address(self):
27 | # Create a customer so that we can test payment creation against him
28 | result = Customer.create()
29 | customer_id = result.customer_id
30 |
31 | # Create a new shipping address
32 | result = Address.create(customer_id, ADDRESS)
33 | address_id = result.address_id
34 |
35 | result = Address.details(customer_id, address_id)
36 | # Compare the address without the customer_address_id
37 | del result.address['address_id']
38 | self.assertEquals(ADDRESS, result.address)
39 |
40 | Address.update(customer_id, address_id, ADDRESS)
41 |
42 | # Delete the address and make sure it is deleted by attempting to
43 | # delete it again.
44 | Address.delete(customer_id, address_id)
45 | self.assertRaises(AuthorizeResponseError, Address.delete, customer_id, address_id)
46 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | Py-Authorize
2 | ============
3 |
4 | Py-Authorize is a full-featured Python API for the Authorize.net payment
5 | gateway. Authorize.net offers great payment processing capabilities with a
6 | terribly incoherent API. Py-Authorize attempts to alleviate many of the
7 | problems programmers might experience with Authorize.net's'API by providing a
8 | cleaner, simpler and much more coherent API.
9 |
10 | Py-Authorize supports most all of the Authorize.net's API functionality
11 | including:
12 |
13 | - Advanced Integration Method (AIM)
14 | - Customer Integration Manager (CIM)
15 | - Transaction Detail API/Reporting
16 | - Automated Recurring Billing API (ARB)
17 |
18 | Here is a simple example of a basic credit card transaction.
19 |
20 | .. code-block:: python
21 |
22 | import authorize
23 |
24 | authorize.Configuration.configure(
25 | authorize.Environment.TEST,
26 | 'api_login_id',
27 | 'api_transaction_key',
28 | )
29 |
30 | result = authorize.Transaction.sale({
31 | 'amount': 40.00,
32 | 'credit_card': {
33 | 'card_number': '4111111111111111',
34 | 'expiration_date': '04/2014',
35 | 'card_code': '343',
36 | }
37 | })
38 |
39 | result.transaction_response.trans_id
40 | # e.g. '2194343352'
41 |
42 | Py-Authorize is released under the `MIT License`_.
43 |
44 | .. _MIT License: http://www.opensource.org/licenses/mit-license
45 |
46 |
47 | Contents:
48 |
49 | .. toctree::
50 | :maxdepth: 1
51 |
52 | install
53 | getting_started
54 | transaction
55 | customer
56 | credit_card
57 | bank_account
58 | pay_pal
59 | address
60 | recurring
61 | batch
62 | advanced
63 | development
64 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Py-Authorize
2 | ============
3 |
4 | Py-Authorize is a full-featured Python API for the Authorize.net payment
5 | gateway. Authorize.net offers great payment processing capabilities with a
6 | terribly incoherent API. Py-Authorize attempts to alleviate many of the
7 | problems programmers might experience with Authorize.net's'API by providing a
8 | cleaner, simpler and much more coherent API.
9 |
10 | Py-Authorize supports most all of the Authorize.net's API functionality
11 | including:
12 |
13 | - Advanced Integration Method (AIM)
14 | - Customer Integration Manager (CIM)
15 | - Transaction Detail API/Reporting
16 | - Automated Recurring Billing API (ARB)
17 |
18 | Here is a simple example of a basic credit card transaction.
19 |
20 | .. code-block:: python
21 |
22 | import authorize
23 |
24 | authorize.Configuration.configure(
25 | authorize.Environment.TEST,
26 | 'api_login_id',
27 | 'api_transaction_key',
28 | )
29 |
30 | result = authorize.Transaction.sale({
31 | 'amount': 40.00,
32 | 'credit_card': {
33 | 'card_number': '4111111111111111',
34 | 'expiration_date': '04/2014',
35 | 'card_code': '343',
36 | }
37 | })
38 |
39 | result.transaction_response.trans_id
40 | # e.g. '2194343352'
41 |
42 |
43 | Documentation
44 | -------------
45 |
46 | Please visit the `Github Page`_ for full documentation.
47 |
48 | .. _Github Page: http://vcatalano.github.io/py-authorize/index.html
49 |
50 |
51 | License
52 | -------
53 |
54 | Py-Authorize is distributed under the `MIT license
55 | `_.
56 |
57 |
58 | Support
59 | -------
60 |
61 | All bug reports, new feature requests and pull requests are handled through
62 | this project's `Github issues`_ page.
63 |
64 | .. _Github issues: https://github.com/vcatalano/py-authorize/issues
--------------------------------------------------------------------------------
/tests/test_batch_api.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | from authorize import Configuration
4 | from authorize.xml_data import prettify
5 |
6 | from unittest import TestCase
7 |
8 | LIST_BATCH_DATES = {
9 | 'start': datetime.datetime(datetime.date.today().year - 1, 5, 1), #'2012-05-01T00:00:00'
10 | 'end': datetime.datetime(datetime.date.today().year - 1, 5, 31), #'2012-05-31T00:00:00'
11 | }
12 |
13 | BATCH_DETAILS_REQUEST = '''
14 |
15 |
16 |
17 | 8s8tVnG5t
18 | 5GK7mncw8mG2946z
19 |
20 | 879802352356
21 |
22 | '''
23 |
24 | LIST_BATCH_REQUEST = '''
25 |
26 |
27 |
28 | 8s8tVnG5t
29 | 5GK7mncw8mG2946z
30 |
31 | true
32 | {}
33 | {}
34 |
35 | '''.format(
36 | LIST_BATCH_DATES['start'].strftime("%Y-%m-%dT%X"),
37 | LIST_BATCH_DATES['end'].strftime("%Y-%m-%dT%X")
38 | )
39 |
40 |
41 | class BatchAPITests(TestCase):
42 |
43 | maxDiff = None
44 |
45 | def test_batch_details_request(self):
46 | request_xml = Configuration.api.batch._details_request('879802352356')
47 | request_string = prettify(request_xml)
48 | self.assertEqual(request_string, BATCH_DETAILS_REQUEST.strip())
49 |
50 | def test_list_batch_request(self):
51 | request_xml = Configuration.api.batch._list_request(LIST_BATCH_DATES)
52 | request_string = prettify(request_xml)
53 | self.assertEqual(request_string, LIST_BATCH_REQUEST.strip())
54 |
--------------------------------------------------------------------------------
/authorize/apis/payment_profile_api.py:
--------------------------------------------------------------------------------
1 | import xml.etree.cElementTree as E
2 |
3 | from authorize.apis.base_api import BaseAPI
4 | from authorize.xml_data import *
5 |
6 |
7 | class PaymentProfileAPI(BaseAPI):
8 |
9 | def details(self, customer_id, payment_id):
10 | return self.api._make_call(self._details_request(customer_id, payment_id))
11 |
12 | def delete(self, customer_id, payment_id):
13 | self.api._make_call(self._delete_request(customer_id, payment_id))
14 |
15 | # The following methods generate the XML for the corresponding API calls.
16 | # This makes unit testing each of the calls easier.
17 | def _details_request(self, customer_id, payment_id):
18 | request = self.api._base_request('getCustomerPaymentProfileRequest')
19 | E.SubElement(request, 'customerProfileId').text = customer_id
20 | E.SubElement(request, 'customerPaymentProfileId').text = payment_id
21 | E.SubElement(request, 'unmaskExpirationDate').text = 'true'
22 | return request
23 |
24 | def _delete_request(self, customer_id, payment_id):
25 | request = self.api._base_request('deleteCustomerPaymentProfileRequest')
26 | E.SubElement(request, 'customerProfileId').text = customer_id
27 | E.SubElement(request, 'customerPaymentProfileId').text = payment_id
28 | return request
29 |
30 | def _make_xml(self, method, customer_id=None, payment_id=None, params={}):
31 | request = self.api._base_request(method)
32 |
33 | if customer_id:
34 | E.SubElement(request, 'customerProfileId').text = customer_id
35 |
36 | profile = E.Element('paymentProfile')
37 |
38 | if 'customer_type' in params:
39 | E.SubElement(profile, 'customerType').text = params['customer_type']
40 |
41 | if 'billing' in params:
42 | profile.append(create_address('billTo', params['billing']))
43 |
44 | profile.append(create_payment(params))
45 | request.append(profile)
46 |
47 | if payment_id:
48 | E.SubElement(profile, 'customerPaymentProfileId').text = payment_id
49 |
50 | return request
51 |
--------------------------------------------------------------------------------
/docs/advanced.rst:
--------------------------------------------------------------------------------
1 | Advanced
2 | ========
3 |
4 | Out-of-the-box Py-Authorize provides some very basic and powerful
5 | functionality. For some users and applications, more advanced API
6 | functionality may be needed. This sections provides an overview and
7 | documentation for some of those features.
8 |
9 | .. _apis:
10 |
11 | APIs
12 | ----
13 |
14 | When configuring Py-Authorize with the `Configuration` global variable,
15 | you are actually instantiating a single instance of the `authorize_api`
16 | class. `authorize.Address`, `authorize.BankAccount`, `authorize.CreditCard`,
17 | `authorize.Customer` and `authorize.Recurring` are all wrappers for
18 | accessing this globally configured API. You can access the API explicitly
19 | through the `Configuration.api` class member. For example, to perform a
20 | basic sale transaction with a credit card using the API you would use the
21 | following method:
22 |
23 | .. code-block:: python
24 |
25 | result = authorize.Configuration.api.transaction.sale({
26 | 'amount': 40.00,
27 | 'credit_card': {
28 | 'card_number': '4111111111111111',
29 | 'expiration_date': '04/2014',
30 | }
31 | })
32 |
33 | Each `authorize_api` instance contains the following members for performing API
34 | calls:
35 |
36 | - ``api.customer``
37 | - ``api.credit_card``
38 | - ``api.bank_account``
39 | - ``api.address``
40 | - ``api.recurring``
41 | - ``api.batch``
42 | - ``api.transaction``
43 |
44 |
45 | Multiple Gateway Configurations
46 | -------------------------------
47 |
48 | For some payment applications, there may be a need to support multiple
49 | gateways. With Py-Authorize, you can instantiate any number of payment
50 | gateway configurations.
51 |
52 | Example
53 | ~~~~~~~
54 |
55 | .. code-block:: python
56 |
57 | configuration_1 = authorize.Configuration(
58 | Environment.PRODUCTION,
59 | 'api_login_id',
60 | 'api_transaction_key',
61 | )
62 |
63 | configuration_2 = authorize.Configuration(
64 | Environment.PRODUCTION,
65 | 'another_api_login_key',
66 | 'another_api_transaction_key',
67 | )
68 |
69 | Once a new configuration has been created, you can make use of each
70 | configuration object's `api` members as outlined in :ref:`APIs `
71 |
72 |
73 |
--------------------------------------------------------------------------------
/authorize/apis/credit_card_api.py:
--------------------------------------------------------------------------------
1 | from authorize.apis.payment_profile_api import PaymentProfileAPI
2 | from authorize.schemas import CreateCreditCardSchema
3 | from authorize.schemas import UpdateCreditCardSchema
4 | from authorize.schemas import ValidateCreditCardSchema
5 | from authorize.xml_data import *
6 |
7 |
8 | class CreditCardAPI(PaymentProfileAPI):
9 |
10 | def create(self, customer_id, params={}):
11 | card = self._deserialize(CreateCreditCardSchema(), params)
12 | return self.api._make_call(self._create_request(customer_id, card))
13 |
14 | def update(self, customer_id, payment_id, params={}):
15 | card = self._deserialize(UpdateCreditCardSchema(), params)
16 | return self.api._make_call(self._update_request(customer_id, payment_id, card))
17 |
18 | def validate(self, customer_id, payment_id, params={}):
19 | card = self._deserialize(ValidateCreditCardSchema(), params)
20 | return self.api._make_call(self._validate_request(customer_id, payment_id, card))
21 |
22 | # The following methods generate the XML for the corresponding API calls.
23 | # This makes unit testing each of the calls easier.
24 | def _create_request(self, customer_id, card={}):
25 | return self._make_xml('createCustomerPaymentProfileRequest', customer_id, None, params=card)
26 |
27 | def _update_request(self, customer_id, payment_id, card={}):
28 |
29 | # Issue 30: If only the last 4 digits of a credit card are provided,
30 | # add the additional XXXX character mask that is required.
31 | if len(card['card_number']) == 4:
32 | card['card_number'] = 'XXXX' + card['card_number']
33 |
34 | return self._make_xml('updateCustomerPaymentProfileRequest', customer_id, payment_id, params=card)
35 |
36 | def _validate_request(self, customer_id, payment_id, card={}):
37 | request = self.api._base_request('validateCustomerPaymentProfileRequest')
38 | E.SubElement(request, 'customerProfileId').text = customer_id
39 | E.SubElement(request, 'customerPaymentProfileId').text = payment_id
40 |
41 | if 'address_id' in card:
42 | E.SubElement(request, 'customerShippingAddressId').text = card['address_id']
43 |
44 | if 'card_code' in card:
45 | E.SubElement(request, 'cardCode').text = str(card['card_code'])
46 |
47 | E.SubElement(request, 'validationMode').text = card['validation_mode']
48 |
49 | return request
50 |
--------------------------------------------------------------------------------
/authorize/apis/address_api.py:
--------------------------------------------------------------------------------
1 | import xml.etree.cElementTree as E
2 |
3 | from authorize.apis.base_api import BaseAPI
4 | from authorize.schemas import AddressSchema
5 | from authorize.xml_data import *
6 |
7 |
8 | class AddressAPI(BaseAPI):
9 |
10 | def create(self, customer_id, params={}):
11 | address = self._deserialize(AddressSchema(), params)
12 | return self.api._make_call(self._create_request(customer_id, address))
13 |
14 | def details(self, customer_id, address_id):
15 | return self.api._make_call(self._details_request(customer_id, address_id))
16 |
17 | def update(self, customer_id, address_id, params={}):
18 | address = self._deserialize(AddressSchema(), params)
19 | return self.api._make_call(self._update_request(customer_id, address_id, address))
20 |
21 | def delete(self, customer_id, address_id):
22 | self.api._make_call(self._delete_request(customer_id, address_id))
23 |
24 | # The following methods generate the XML for the corresponding API calls.
25 | # This makes unit testing each of the calls easier.
26 | def _create_request(self, customer_id, address={}):
27 | return self._make_xml('createCustomerShippingAddressRequest', customer_id, None, params=address)
28 |
29 | def _details_request(self, customer_id, address_id):
30 | request = self.api._base_request('getCustomerShippingAddressRequest')
31 | E.SubElement(request, 'customerProfileId').text = customer_id
32 | E.SubElement(request, 'customerAddressId').text = address_id
33 | return request
34 |
35 | def _update_request(self, customer_id, address_id, address={}):
36 | return self._make_xml('updateCustomerShippingAddressRequest', customer_id, address_id, params=address)
37 |
38 | def _delete_request(self, customer_id, address_id):
39 | request = self.api._base_request('deleteCustomerShippingAddressRequest')
40 | E.SubElement(request, 'customerProfileId').text = customer_id
41 | E.SubElement(request, 'customerAddressId').text = address_id
42 | return request
43 |
44 | def _make_xml(self, method, customer_id=None, address_id=None, params={}):
45 | request = self.api._base_request(method)
46 |
47 | if customer_id:
48 | E.SubElement(request, 'customerProfileId').text = customer_id
49 |
50 | address_data = create_address('address', params)
51 |
52 | if address_id:
53 | E.SubElement(address_data, 'customerAddressId').text = address_id
54 |
55 | request.append(address_data)
56 |
57 | return request
58 |
--------------------------------------------------------------------------------
/docs/development.rst:
--------------------------------------------------------------------------------
1 | Development
2 | ===========
3 |
4 | Any development help is greatly appreciated. If you have have any new
5 | features, bug fixes or documentation improvements please feel free to
6 | contribute.
7 |
8 |
9 | Getting started
10 | ---------------
11 |
12 | To start developing on this project, fork this project on our `Github page`_
13 | and install from source using the instructions in :doc:`Install `.
14 | Additionally, you will need to install the following dependencies for running
15 | test and compiling documentation.
16 |
17 | - nose_
18 | - tox_ (for testing multiple versions of Python)
19 | - sphinx_ (for documentation)
20 | - sphinx_rtd_theme_ (documentation theme)
21 |
22 | .. _Github page: https://github.com/vcatalano/py-authorize
23 | .. _nose: https://nose.readthedocs.org/en/latest/
24 | .. _tox: https://tox.readthedocs.io/en/latest/
25 | .. _sphinx: http://sphinx-doc.org/
26 | .. _sphinx_rtd_theme: https://github.com/snide/sphinx_rtd_theme
27 |
28 |
29 | Running Tests
30 | -------------
31 |
32 | This project has been configured to use the Nose testing framework and Tox
33 | for automation. The following command will run all tests for the project.
34 | Since many of the tests connect to the Authorize.net server, running the
35 | tests may take quite a few seconds.
36 |
37 | .. code-block:: bash
38 |
39 | nosetests
40 |
41 | To run only local tests, you can use the following command:
42 |
43 | .. code-block:: bash
44 |
45 | nosetests -a '!live_tests'
46 |
47 | To local tests for Python versions 2.7, 3.3, 3.4, 3.5 and PyPy:
48 |
49 | .. code-block:: bash
50 |
51 | tox
52 |
53 |
54 | Authorize.net documentation
55 | ---------------------------
56 |
57 | The Authorize.net documentation can be overly verbose and very inconsistent
58 | with the implementations of many of its features. You can view the
59 | documentation by visiting the following links:
60 |
61 | - `Developer site`_
62 | - `Advanced Integration Method`_
63 | - `Customer Information Manager`_
64 | - `Automated Recurring Billing`_
65 |
66 | .. _Developer site: http://developer.authorize.net/
67 | .. _Advanced Integration Method: http://www.authorize.net/support/AIM_guide_XML.pdf
68 | .. _Customer Information Manager: http://www.authorize.net/support/CIM_XML_guide.pdf
69 | .. _Automated Recurring Billing: http://www.authorize.net/support/ARB_guide.pdf
70 |
71 |
72 | Submitting bugs and patches
73 | ---------------------------
74 |
75 | All bug reports, new feature requests and pull requests are handled through
76 | this project's `Github issues`_ page.
77 |
78 | .. _Github issues: https://github.com/vcatalano/py-authorize/issues
79 |
--------------------------------------------------------------------------------
/tests/test_live_bank_account.py:
--------------------------------------------------------------------------------
1 | from authorize import BankAccount
2 | from authorize import Customer
3 | from authorize import AuthorizeResponseError
4 |
5 | from nose.plugins.attrib import attr
6 |
7 | from unittest import TestCase
8 |
9 | BANK_ACCOUNT = {
10 | 'routing_number': '322271627',
11 | 'account_number': '00987467838473',
12 | 'name_on_account': 'Rob Otron',
13 | }
14 |
15 | FULL_BANK_ACCOUNT = {
16 | 'customer_type': 'individual',
17 | 'account_type': 'checking',
18 | 'routing_number': '322271627',
19 | 'account_number': '00987467838473',
20 | 'name_on_account': 'Rob Otron',
21 | 'bank_name': 'Evil Bank Co.',
22 | 'echeck_type': 'CCD',
23 | 'billing': {
24 | 'first_name': 'Rob',
25 | 'last_name': 'Oteron',
26 | 'company': 'Robotron Studios',
27 | 'address': '101 Computer Street',
28 | 'city': 'Tucson',
29 | 'state': 'AZ',
30 | 'zip': '85704',
31 | 'country': 'US',
32 | 'phone_number': '520-123-4567',
33 | 'fax_number': '520-456-7890',
34 | },
35 | }
36 |
37 | PAYMENT_RESULT = {
38 | 'bank_account': {
39 | 'account_type': 'checking',
40 | 'routing_number': 'XXXX1627',
41 | 'account_number': 'XXXX8473',
42 | 'name_on_account': 'Rob Otron',
43 | 'echeck_type': 'WEB',
44 | }
45 | }
46 |
47 |
48 | @attr('live_tests')
49 | class BankAccountTests(TestCase):
50 |
51 | def test_live_bank_account(self):
52 | # Create a customer so that we can test payment creation against him
53 | result = Customer.create()
54 | customer_id = result.customer_id
55 |
56 | # Create a new bank account
57 | result = BankAccount.create(customer_id, BANK_ACCOUNT)
58 | payment_id = result.payment_id
59 |
60 | # Read credit card data
61 | result = BankAccount.details(customer_id, payment_id)
62 | self.assertEquals(PAYMENT_RESULT, result.payment_profile.payment)
63 |
64 | # Update credit card
65 | BankAccount.update(customer_id, payment_id, BANK_ACCOUNT)
66 |
67 | # Delete tests
68 | BankAccount.delete(customer_id, payment_id)
69 | self.assertRaises(AuthorizeResponseError, BankAccount.delete, customer_id, payment_id)
70 |
71 | def test_live_full_bank_account(self):
72 | # Create a customer so that we can test payment creation against him
73 | result = Customer.create()
74 | customer_id = result.customer_id
75 |
76 | # Create a new bank account
77 | result = BankAccount.create(customer_id, FULL_BANK_ACCOUNT)
78 | payment_id = result.payment_id
79 |
80 | # Make sure the billing address we set is the same we get back
81 | result = BankAccount.details(customer_id, payment_id)
82 | self.assertEquals(FULL_BANK_ACCOUNT['billing'], result.payment_profile.bill_to)
83 |
--------------------------------------------------------------------------------
/docs/address.rst:
--------------------------------------------------------------------------------
1 | Address
2 | =======
3 |
4 | The Address API manages customer shipping addresses for Authorize.net's
5 | Customer Information Manager (CIM). Addresses must be associated to a
6 | customer profile on the Authorize.net server. An address can be associated
7 | when a new customer is created, to see how this is handled refer to the
8 | :doc:`Customer API` documentation.
9 |
10 | Create
11 | ------
12 |
13 | To associate an address to an existing customer use the `create` method.
14 | When creating an address, you must provide the customer profile ID as the
15 | first argument. No fields are required when creating an address, however, at
16 | least one field must be provided.
17 |
18 | .. code-block:: python
19 |
20 | result = authorize.Address.create('customer_id', {
21 | 'first_name': 'Rob',
22 | 'last_name': 'Oteron',
23 | 'company': 'Robotron Studios',
24 | 'address': '101 Computer Street',
25 | 'city': 'Tucson',
26 | 'state': 'AZ',
27 | 'zip': '85704',
28 | 'country': 'US',
29 | 'phone_number': '520-123-4567',
30 | 'fax_number': '520-456-7890',
31 | })
32 |
33 | result.address_id
34 | # e.g. '17769620'
35 |
36 |
37 | Details
38 | -------
39 |
40 | The `details` method returns the information for a given customer address.
41 | You must provide the both customer profile ID and the customer address ID
42 | respectively.
43 |
44 | The following information is returnd in the result attribute dictionary:
45 |
46 | - ``address.first_name``
47 | - ``address.last_name``
48 | - ``address.company``
49 | - ``address.address``
50 | - ``address.city``
51 | - ``address.state``
52 | - ``address.zip``
53 | - ``address.country``
54 | - ``address.phone_number``
55 | - ``address.fax_number``
56 |
57 | .. code-block:: python
58 |
59 | result = authorize.Address.details('customer_id', '17769620')
60 |
61 | result.address_id
62 | # e.g. '17769620'
63 |
64 |
65 | Update
66 | ------
67 |
68 | The ``update`` method will update the address information for a given
69 | address ID. The method requires the customer profile ID, the customer
70 | address ID and the updated customer address information.
71 |
72 | .. code-block:: python
73 |
74 | result = authorize.Address.create('customer_id', '17769620', {
75 | 'first_name': 'Rob',
76 | 'last_name': 'Oteron',
77 | 'company': 'Robotron Studios',
78 | 'address': '101 Computer Street',
79 | 'city': 'Tucson',
80 | 'state': 'AZ',
81 | 'zip': '85704',
82 | 'country': 'US',
83 | 'phone_number': '520-123-4567',
84 | 'fax_number': '520-456-7890',
85 | })
86 |
87 |
88 | Delete
89 | ------
90 |
91 | Deleting a customer address will remove the address information associated
92 | the customer.
93 |
94 | .. code-block:: python
95 |
96 | authorize.Address.delete('customer_id', '17769620')
--------------------------------------------------------------------------------
/authorize/apis/authorize_api.py:
--------------------------------------------------------------------------------
1 | import xml.etree.cElementTree as E
2 |
3 | try:
4 | import urllib.request as urllib2
5 | except:
6 | import urllib2
7 |
8 | from authorize.apis.address_api import AddressAPI
9 | from authorize.apis.credit_card_api import CreditCardAPI
10 | from authorize.apis.customer_api import CustomerAPI
11 | from authorize.apis.bank_account_api import BankAccountAPI
12 | from authorize.apis.batch_api import BatchAPI
13 | from authorize.apis.recurring_api import RecurringAPI
14 | from authorize.apis.transaction_api import TransactionAPI
15 | from authorize.exceptions import AuthorizeConnectionError
16 | from authorize.exceptions import AuthorizeResponseError
17 | from authorize.response_parser import parse_response
18 | from authorize.xml_data import *
19 |
20 | E.register_namespace('', 'AnetApi/xml/v1/schema/AnetApiSchema.xsd')
21 |
22 |
23 | class AuthorizeAPI(object):
24 |
25 | def __init__(self, config):
26 | """Allow for multiple instances of the Authorize API."""
27 | self.config = config
28 | self.customer = CustomerAPI(self)
29 | self.credit_card = CreditCardAPI(self)
30 | self.bank_account = BankAccountAPI(self)
31 | self.address = AddressAPI(self)
32 | self.recurring = RecurringAPI(self)
33 | self.batch = BatchAPI(self)
34 | self.transaction = TransactionAPI(self)
35 | self._client_auth = None
36 |
37 | @property
38 | def client_auth(self):
39 | """Generate an XML element with client auth data populated."""
40 | if not self._client_auth:
41 | self._client_auth = E.Element('merchantAuthentication')
42 | E.SubElement(self._client_auth, 'name').text = self.config.login_id
43 | E.SubElement(self._client_auth, 'transactionKey').text = self.config.transaction_key
44 | return self._client_auth
45 |
46 | def _base_request(self, method):
47 | """Factory method for generating the base XML requests."""
48 | request = E.Element(method)
49 | request.set('xmlns', 'AnetApi/xml/v1/schema/AnetApiSchema.xsd')
50 | request.append(self.client_auth)
51 | return request
52 |
53 | def _make_call(self, call):
54 | """Make a call to the Authorize.net server with the XML."""
55 | try:
56 | request = urllib2.Request(self.config.environment, E.tostring(call))
57 | request.add_header('Content-Type', 'text/xml')
58 | response = urllib2.urlopen(request).read()
59 | response = E.fromstring(response)
60 | response_json = parse_response(response)
61 | except urllib2.HTTPError:
62 | raise AuthorizeConnectionError('Error processing XML request.')
63 |
64 | # Exception handling for transaction response errors.
65 | try:
66 | error = response_json.transaction_response.errors[0]
67 | raise AuthorizeResponseError(error.error_code, error.error_text, response_json)
68 | except (KeyError, AttributeError): # Attempt to access transaction response errors
69 | pass
70 |
71 | # Throw an exception for invalid calls. This makes error handling easier.
72 | if response_json.messages[0].result_code != 'Ok':
73 | error = response_json.messages[0].message
74 | raise AuthorizeResponseError(error.code, error.text, response_json)
75 |
76 | return response_json
77 |
--------------------------------------------------------------------------------
/tests/test_live_customer.py:
--------------------------------------------------------------------------------
1 | import random
2 | from authorize import Customer, Transaction
3 | from authorize import AuthorizeResponseError
4 |
5 | from datetime import date
6 |
7 | from nose.plugins.attrib import attr
8 |
9 | from unittest import TestCase
10 |
11 | FULL_CUSTOMER = {
12 | 'email': 'vincent@vincentcatalano.com',
13 | 'description': 'Cool web developer guy',
14 | 'customer_type': 'individual',
15 | 'billing': {
16 | 'first_name': 'Rob',
17 | 'last_name': 'Oteron',
18 | 'company': 'Robotron Studios',
19 | 'address': '101 Computer Street',
20 | 'city': 'Tucson',
21 | 'state': 'AZ',
22 | 'zip': '85704',
23 | 'country': 'US',
24 | 'phone_number': '520-123-4567',
25 | 'fax_number': '520-456-7890',
26 | },
27 | 'bank_account': {
28 | 'routing_number': '322271627',
29 | 'account_number': '00987467838473',
30 | 'name_on_account': 'Rob Otron',
31 | 'bank_name': 'Evil Bank Co.',
32 | 'echeck_type': 'CCD'
33 | },
34 | 'shipping': {
35 | 'first_name': 'Rob',
36 | 'last_name': 'Oteron',
37 | 'company': 'Robotron Studios',
38 | 'address': '101 Computer Street',
39 | 'city': 'Tucson',
40 | 'state': 'AZ',
41 | 'zip': '85704',
42 | 'country': 'US',
43 | 'phone_number': '520-123-4567',
44 | 'fax_number': '520-456-7890',
45 | }
46 | }
47 |
48 | CUSTOMER_WITH_CARD = {
49 | 'email': 'vincent@vincentcatalano.com',
50 | 'description': 'Cool web developer guy',
51 | 'credit_card': {
52 | 'card_number': '4111111111111111',
53 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
54 | 'card_code': '456',
55 | },
56 | }
57 |
58 |
59 | @attr('live_tests')
60 | class CustomerTests(TestCase):
61 |
62 | def test_live_customer(self):
63 | # Create customers
64 | result = Customer.create()
65 | Customer.create(FULL_CUSTOMER)
66 | Customer.create(CUSTOMER_WITH_CARD)
67 |
68 | # Read customer information. This returns the payment profile IDs
69 | # address IDs for the user
70 | customer_id = result.customer_id
71 | Customer.details(customer_id)
72 |
73 | # Update customer information
74 | Customer.update(customer_id, {
75 | 'email': 'vincent@test.com',
76 | 'description': 'Cool web developer guy'
77 | })
78 |
79 | # Delete customer information
80 | Customer.delete(customer_id)
81 | self.assertRaises(AuthorizeResponseError, Customer.delete, customer_id)
82 |
83 | Customer.list()
84 |
85 | def test_live_customer_from_transaction(self):
86 |
87 | INVALID_TRANS_ID = '123'
88 |
89 | self.assertRaises(AuthorizeResponseError, Customer.from_transaction, INVALID_TRANS_ID)
90 |
91 | # Create the transaction
92 | transaction = CUSTOMER_WITH_CARD.copy()
93 | transaction['amount'] = random.randrange(100, 100000) / 100.0
94 | result = Transaction.auth(transaction)
95 | trans_id = result.transaction_response.trans_id
96 |
97 | # Create the customer from the above transaction
98 | result = Customer.from_transaction(trans_id)
99 | customer_id = result.customer_id
100 | result = Customer.details(customer_id)
101 |
102 | self.assertEquals(transaction['email'], result.profile.email)
103 |
--------------------------------------------------------------------------------
/authorize/response_parser.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 |
4 |
5 | RENAME_FIELDS = {
6 | 'customerProfileId': 'customer_id',
7 | 'customerPaymentProfileId': 'payment_id',
8 | 'customerAddressId': 'address_id',
9 | 'customerShippingAddressId': 'address_id',
10 | 'customerPaymentProfileIdList': 'payment_ids',
11 | 'customerShippingAddressIdList': 'address_ids',
12 | 'validationDirectResponseList': 'validation_responses',
13 | 'purchaseOrderNumber': 'order_number',
14 | 'paymentProfiles': 'payments',
15 | 'shipToList': 'addresses',
16 | 'ids': 'profile_ids',
17 | 'shipping': 'shipping_and_handling',
18 | 'directResponse': 'transaction_response',
19 | }
20 |
21 | # XML Schema sequence element
22 | LIST_FIELDS = [
23 | 'messages',
24 | 'shipToList',
25 | 'paymentProfiles',
26 | ]
27 |
28 | # Array of complex schema types
29 | NESTED_LIST_FIELDS = [
30 | 'ids',
31 | 'errors',
32 | 'customerPaymentProfileIdList',
33 | 'customerShippingAddressIdList',
34 | 'transactions',
35 | 'batchList',
36 | 'statistics',
37 | 'lineItems',
38 | 'userFields',
39 | 'subscriptionDetails'
40 | ]
41 |
42 | DIRECT_RESPONSE_FIELDS = {
43 | 0: 'response_code',
44 | 2: 'response_reason_code',
45 | 3: 'response_reason_text',
46 | 4: 'authorization_code',
47 | 5: 'avs_response',
48 | 6: 'trans_id',
49 | 9: 'amount',
50 | 11: 'transaction_type',
51 | 38: 'cvv_result_code',
52 | }
53 |
54 | FIRST_CAP_RE = re.compile('(.)([A-Z][a-z]+)')
55 | ALL_CAP_RE = re.compile('([a-z0-9])([A-Z])')
56 |
57 |
58 | class AttrDict(dict):
59 |
60 | def __init__(self, *args, **kw):
61 | dict.__init__(self, *args, **kw)
62 |
63 | def __getattr__(self, key):
64 | try:
65 | return self[key]
66 | except KeyError:
67 | raise AttributeError(key)
68 |
69 | def __setattr__(self, key, value):
70 | self[key] = value
71 |
72 | def __str__(self):
73 | return json.dumps(self, indent=2)
74 |
75 |
76 | # Use lower_case_with_underscores to keep key names consistent
77 | def rename(name):
78 | if name in RENAME_FIELDS:
79 | name = RENAME_FIELDS[name]
80 | name = FIRST_CAP_RE.sub(r'\1_\2', name)
81 | name = ALL_CAP_RE.sub(r'\1_\2', name)
82 | return name.lower()
83 |
84 |
85 | def parse_direct_response(response_text):
86 | response = response_text.text.split(',')
87 | fields = AttrDict()
88 | for index, name in DIRECT_RESPONSE_FIELDS.items():
89 | fields[name] = response[index]
90 | return fields
91 |
92 |
93 | def parse_response(element):
94 | # Remove the namespace qualifier
95 | key = element.tag[41:]
96 |
97 | if key == 'directResponse':
98 | return parse_direct_response(element)
99 |
100 | if len(element) == 0:
101 | return element.text
102 |
103 | dict_items = AttrDict()
104 | is_nested = key in NESTED_LIST_FIELDS
105 |
106 | for child in element:
107 | key = child.tag[41:]
108 | new_item = parse_response(child)
109 |
110 | if is_nested:
111 | # Ignore the current element and treat as a list item
112 | if isinstance(dict_items, list):
113 | dict_items.append(new_item)
114 | else:
115 | dict_items = [new_item]
116 | elif key in LIST_FIELDS:
117 | try:
118 | dict_items[rename(key)].append(new_item)
119 | except:
120 | dict_items[rename(key)] = [new_item]
121 | else:
122 | dict_items[rename(key)] = new_item
123 |
124 | return dict_items
125 |
--------------------------------------------------------------------------------
/tests/test_live_credit_card.py:
--------------------------------------------------------------------------------
1 | from authorize import CreditCard
2 | from authorize import Customer
3 | from authorize import AuthorizeResponseError
4 |
5 | from datetime import date
6 |
7 | from nose.plugins.attrib import attr
8 |
9 | from unittest import TestCase
10 |
11 | CREDIT_CARD = {
12 | 'card_number': '4111111111111111',
13 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
14 | 'card_code': '456',
15 | }
16 |
17 | FULL_CREDIT_CARD = {
18 | 'card_number': '4111111111111111',
19 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
20 | 'card_code': '456',
21 | 'billing': {
22 | 'first_name': 'Rob',
23 | 'last_name': 'Oteron',
24 | 'company': 'Robotron Studios',
25 | 'address': '101 Computer Street',
26 | 'city': 'Tucson',
27 | 'state': 'AZ',
28 | 'zip': '85704',
29 | 'country': 'US',
30 | 'phone_number': '520-123-4567',
31 | 'fax_number': '520-456-7890',
32 | },
33 | }
34 |
35 | UPDATE_CREDIT_CARD = {
36 | 'card_number': '5555555555554444',
37 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
38 | 'card_code': '567',
39 | }
40 |
41 | UPDATE_CREDIT_CARD_WITH_MASK = {
42 | 'card_number': 'XXXX4444',
43 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
44 | 'card_code': '567',
45 | }
46 |
47 | UPDATE_CREDIT_CARD_WITHOUT_MASK = {
48 | 'card_number': '4444',
49 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
50 | 'card_code': '567',
51 | }
52 |
53 | UPDATE_CREDIT_CARD_INVALID_MASK = {
54 | 'card_number': '1111',
55 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
56 | 'card_code': '567',
57 | }
58 |
59 | PAYMENT_RESULT = {
60 | 'credit_card': {
61 | 'card_number': 'XXXX1111',
62 | 'expiration_date': '{0}-04'.format(date.today().year + 1),
63 | 'card_type': 'Visa'
64 | }
65 | }
66 |
67 |
68 | @attr('live_tests')
69 | class CreditCardTests(TestCase):
70 |
71 | def test_live_basic_credit_card(self):
72 | # Create a customer so that we can test payment creation against him
73 | result = Customer.create()
74 | customer_id = result.customer_id
75 |
76 | # Create a new credit card
77 | result = CreditCard.create(customer_id, CREDIT_CARD)
78 | payment_id = result.payment_id
79 |
80 | # Attempt to create a duplicate credit card entry. This will fail
81 | # since it is a duplicate payment method.
82 | self.assertRaises(AuthorizeResponseError, CreditCard.create, customer_id, CREDIT_CARD)
83 |
84 | # Read credit card data
85 | result = CreditCard.details(customer_id, payment_id)
86 | self.assertEquals(PAYMENT_RESULT, result.payment_profile.payment)
87 |
88 | # Update credit card
89 | CreditCard.update(customer_id, payment_id, UPDATE_CREDIT_CARD)
90 | CreditCard.update(customer_id, payment_id, UPDATE_CREDIT_CARD_WITH_MASK)
91 | CreditCard.update(customer_id, payment_id, UPDATE_CREDIT_CARD_WITHOUT_MASK)
92 |
93 | # Invalid masked number
94 | self.assertRaises(AuthorizeResponseError, CreditCard.update, customer_id, payment_id, UPDATE_CREDIT_CARD_INVALID_MASK)
95 |
96 | # Delete tests
97 | CreditCard.delete(customer_id, payment_id)
98 | self.assertRaises(AuthorizeResponseError, CreditCard.delete, customer_id, payment_id)
99 |
100 | def test_live_full_credit_card(self):
101 | # Create a customer so that we can test payment creation against him
102 | result = Customer.create()
103 | customer_id = result.customer_id
104 |
105 | result = CreditCard.create(customer_id, FULL_CREDIT_CARD)
106 | payment_id = result.payment_id
107 |
108 | # Make sure the billing address we set is the same we get back
109 | result = CreditCard.details(customer_id, payment_id)
110 | self.assertEquals(FULL_CREDIT_CARD['billing'], result.payment_profile.bill_to)
111 |
112 | # Validate the credit card information
113 | result = CreditCard.validate(customer_id, payment_id, {
114 | 'card_code': '456',
115 | 'validation_mode': 'testMode',
116 | })
117 |
--------------------------------------------------------------------------------
/tests/test_address_api.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 | from authorize.xml_data import prettify
3 |
4 | from unittest import TestCase
5 |
6 | ADDRESS = {
7 | 'first_name': 'Rob',
8 | 'last_name': 'Oteron',
9 | 'company': 'Robotron Studios',
10 | 'address': '101 Computer Street',
11 | 'city': 'Tucson',
12 | 'state': 'AZ',
13 | 'zip': '85704',
14 | 'country': 'US',
15 | 'phone_number': '520-123-4567',
16 | 'fax_number': '520-456-7890',
17 | }
18 |
19 | CREATE_ADDRESS_REQUEST = '''
20 |
21 |
22 |
23 | 8s8tVnG5t
24 | 5GK7mncw8mG2946z
25 |
26 | 1234567890
27 |
28 | Rob
29 | Oteron
30 | Robotron Studios
31 | 101 Computer Street
32 | Tucson
33 | AZ
34 | 85704
35 | US
36 | 520-123-4567
37 | 520-456-7890
38 |
39 | '''
40 |
41 | DETAILS_ADDRESS_REQUEST = '''
42 |
43 |
44 |
45 | 8s8tVnG5t
46 | 5GK7mncw8mG2946z
47 |
48 | 1234567890
49 | 0987654321
50 | '''
51 |
52 | UPDATE_ADDRESS_REQUEST = '''
53 |
54 |
55 |
56 | 8s8tVnG5t
57 | 5GK7mncw8mG2946z
58 |
59 | 1234567890
60 |
61 | Rob
62 | Oteron
63 | Robotron Studios
64 | 101 Computer Street
65 | Tucson
66 | AZ
67 | 85704
68 | US
69 | 520-123-4567
70 | 520-456-7890
71 | 0987654321
72 |
73 | '''
74 |
75 | DELETE_ADDRESS_REQUEST = '''
76 |
77 |
78 |
79 | 8s8tVnG5t
80 | 5GK7mncw8mG2946z
81 |
82 | 1234567890
83 | 0987654321
84 | '''
85 |
86 |
87 | class AddressAPITests(TestCase):
88 |
89 | maxDiff = None
90 |
91 | def test_create_address_request(self):
92 | request_xml = Configuration.api.address._create_request('1234567890', ADDRESS)
93 | request_string = prettify(request_xml)
94 | self.assertEqual(request_string, CREATE_ADDRESS_REQUEST.strip())
95 |
96 | def test_details_address_request(self):
97 | request_xml = Configuration.api.address._details_request('1234567890', '0987654321')
98 | request_string = prettify(request_xml)
99 | self.assertEqual(request_string, DETAILS_ADDRESS_REQUEST.strip())
100 |
101 | def test_update_address_request(self):
102 | request_xml = Configuration.api.address._update_request('1234567890', '0987654321', ADDRESS)
103 | request_string = prettify(request_xml)
104 | self.assertEqual(request_string, UPDATE_ADDRESS_REQUEST.strip())
105 |
106 | def test_delete_address_request(self):
107 | request_xml = Configuration.api.address._delete_request('1234567890', '0987654321')
108 | request_string = prettify(request_xml)
109 | self.assertEqual(request_string, DELETE_ADDRESS_REQUEST.strip())
110 |
--------------------------------------------------------------------------------
/tests/test_schemas.py:
--------------------------------------------------------------------------------
1 | from authorize.schemas import AddressSchema
2 | from authorize.schemas import BankAccountSchema
3 | from authorize.schemas import CreditCardSchema
4 | from authorize.schemas import CreateRecurringSchema
5 |
6 | from colander import Invalid
7 | from datetime import date
8 | from unittest import TestCase
9 |
10 | ADDRESS = {
11 | 'first_name': 'Rob',
12 | 'last_name': 'Oteron',
13 | 'company': 'Robotron Studios',
14 | 'address': '101 Computer Street',
15 | 'city': 'Tucson',
16 | 'state': 'AZ',
17 | 'zip': '85704',
18 | 'country': 'US',
19 | 'phone_number': '520-1234567',
20 | 'fax_number': '520-765-4321',
21 | }
22 |
23 | BASIC_RECURRING = {
24 | 'amount': 40,
25 | 'interval_length': 14,
26 | 'interval_unit': 'days',
27 | 'credit_card': {
28 | 'card_number': '4111111111111111',
29 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
30 | },
31 | }
32 |
33 | INVALID_ADDRESS = {
34 | 'city': 'Tucson',
35 | 'state': 'AZ',
36 | 'zip': '85704',
37 | 'country': 'US',
38 | }
39 |
40 | CREDIT_CARD_EXP_MONTH_AND_YEAR = {
41 | 'card_number': '4111111111111111',
42 | 'card_code': '456',
43 | 'expiration_month': '04',
44 | 'expiration_year': str(date.today().year + 1)
45 | }
46 |
47 | CREDIT_CARD_EXP_DATE = {
48 | 'card_number': '4111111111111111',
49 | 'card_code': '456',
50 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
51 | }
52 |
53 | UPDATE_CREDIT_CARD_EXP_MONTH_AND_YEAR = {
54 | 'card_number': '4111111111111111',
55 | 'card_code': '456',
56 | 'expiration_month': '04',
57 | 'expiration_year': str(date.today().year + 1)
58 | }
59 |
60 | INVALID_CREDIT_CARD_NUMBER = {
61 | 'card_number': 'Bad card number',
62 | 'card_code': '456',
63 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
64 | }
65 |
66 | INVALID_CREDIT_CARD_NO_EXP_DATE = {
67 | 'card_number': '4111111111111111',
68 | 'card_code': '456',
69 | }
70 |
71 | INVALID_CREDIT_CARD_NO_YEAR = {
72 | 'card_number': '4111111111111111',
73 | 'card_code': '456',
74 | 'expiration_month': '04',
75 | }
76 |
77 | INVALID_CREDIT_CARD_NO_MONTH = {
78 | 'card_number': '4111111111111111',
79 | 'card_code': '456',
80 | 'expiration_year': str(date.today().year + 1),
81 | }
82 |
83 | FULL_CREDIT_CARD = {
84 | 'card_number': '4111111111111111',
85 | 'card_code': '456',
86 | 'expiration_month': '04',
87 | 'expiration_year': str(date.today().year + 1),
88 | 'first_name': 'Rob',
89 | 'last_name': 'Oteron',
90 | }
91 |
92 | BASIC_BANK_ACCOUNT = {
93 | 'routing_number': '322271627',
94 | 'account_number': '00987467838473',
95 | 'name_on_account': 'Rob Otron',
96 | }
97 |
98 | FULL_BANK_ACCOUNT = {
99 | 'account_type': 'checking',
100 | 'routing_number': '322271627',
101 | 'account_number': '00987467838473',
102 | 'name_on_account': 'Rob Otron',
103 | 'bank_name': 'Evil Bank Co.',
104 | 'echeck_type': 'CCD',
105 | }
106 |
107 | INVALID_BANK_ACCOUNT_BAD_ROUTING_NUMBER = {
108 | 'routing_number': 'Bad routing number',
109 | 'account_number': '00987467838473',
110 | 'name_on_account': 'Rob Otron',
111 | }
112 |
113 | INVALID_BANK_ACCOUNT_BAD_ACCOUNT_NUMBER = {
114 | 'routing_number': '322271627',
115 | 'account_number': 'Bad account number',
116 | 'name_on_account': 'Rob Otron',
117 | }
118 |
119 |
120 | class SchemaTests(TestCase):
121 |
122 | maxDiff = None
123 |
124 | def test_address_schema(self):
125 | schema = AddressSchema()
126 | schema.deserialize(ADDRESS)
127 |
128 | def test_credit_card_schema(self):
129 | schema = CreditCardSchema()
130 | schema.deserialize(CREDIT_CARD_EXP_MONTH_AND_YEAR)
131 | schema.deserialize(CREDIT_CARD_EXP_DATE)
132 | self.assertRaises(Invalid, schema.deserialize, INVALID_CREDIT_CARD_NUMBER)
133 | self.assertRaises(Invalid, schema.deserialize, INVALID_CREDIT_CARD_NO_EXP_DATE)
134 | self.assertRaises(Invalid, schema.deserialize, INVALID_CREDIT_CARD_NO_MONTH)
135 | schema.deserialize(FULL_CREDIT_CARD)
136 |
137 | def test_bank_account_schema(self):
138 | schema = BankAccountSchema()
139 | schema.deserialize(BASIC_BANK_ACCOUNT)
140 | schema.deserialize(FULL_BANK_ACCOUNT)
141 | self.assertRaises(Invalid, schema.deserialize, INVALID_BANK_ACCOUNT_BAD_ROUTING_NUMBER)
142 | self.assertRaises(Invalid, schema.deserialize, INVALID_BANK_ACCOUNT_BAD_ACCOUNT_NUMBER)
143 |
144 | def test_arb_address_schema(self):
145 | schema = CreateRecurringSchema().bind(arb=True)
146 | schema.deserialize(BASIC_RECURRING)
147 |
--------------------------------------------------------------------------------
/docs/pay_pal.rst:
--------------------------------------------------------------------------------
1 | PayPal Express Checkout
2 | =======================
3 |
4 | Authorize.net now provides functionality for PayPal Express Checkout. With
5 | PayPal Express Checkout, you can accept payments with PayPal while utilizing
6 | Authorize.net's reporting functionality.
7 |
8 | For more detailed information about how the PayPal Express Checkout process
9 | works with Authorize.net, visit the official `PayPal Express Checkout`_
10 | documentation.
11 |
12 | .. _PayPal Express Checkout:http://developer.authorize.net/api/reference/features/paypal.html
13 |
14 |
15 | Additional API Flow Functions
16 | -----------------------------
17 |
18 | In order to handle the additional steps required by the PayPal Express Checkout
19 | flow process, two additional functions have been added to the Transaction API:
20 | ``Transaction.auth_continue`` and ``Transaction.sale_continue``. These functions
21 | refer to ``Authorize Only, Continue`` and ``Authorize and Capture, Continue``
22 | requests, respectively.
23 |
24 |
25 | Transaction Flow Sequence Example 1
26 | -----------------------------------
27 |
28 | #. Authorization Only
29 | #. Get Details (recommended for shipping)
30 | #. Authorization Only, Continue
31 | #. Prior Authorization Capture
32 | #. Refund (optional)
33 |
34 | .. code-block:: python
35 |
36 | result = authorize.Transaction.auth({
37 | 'amount': 40.00,
38 | 'pay_pal': {
39 | 'success_url': 'https://my.server.com/success.html',
40 | 'cancel_url': 'https://my.server.com/cancel.html',
41 | 'locale': 'US',
42 | 'header_image': 'https://usa.visa.com/img/home/logo_visa.gif',
43 | 'flow_color': 'FF0000'
44 | },
45 | })
46 |
47 | result.transaction_response.trans_id
48 | # e.g. 'transaction_id'
49 |
50 | result.secure_acceptance.secure_acceptance_url
51 | # e.g. https://www.paypal.com/cgibin/webscr?cmd=_express-checkout&token=EC-4WL17777V4111184H
52 |
53 | # (optional) get shipping information for order
54 | details = authorize.Transaction.details('transaction_id')
55 |
56 | authorize.Transaction.auth_continue('transaction_id', 'payer_id')
57 |
58 | authorize.Transaction.settle('transaction_id')
59 |
60 | # (optional) refund the transaction
61 | authorize.Transaction.refund('transaction_id')
62 |
63 |
64 | Transaction Flow Sequence Example 2
65 | -----------------------------------
66 |
67 | #. Authorization Only
68 | #. Get Details (recommended for shipping)
69 | #. Authorization Only, Continue
70 | #. Void
71 |
72 | .. code-block:: python
73 |
74 | result = authorize.Transaction.auth({
75 | 'amount': 40.00,
76 | 'pay_pal': {
77 | 'success_url': 'https://my.server.com/success.html',
78 | 'cancel_url': 'https://my.server.com/cancel.html',
79 | 'locale': 'US',
80 | 'header_image': 'https://usa.visa.com/img/home/logo_visa.gif',
81 | 'flow_color': 'FF0000'
82 | },
83 | })
84 |
85 | result.transaction_response.trans_id
86 | # e.g. 'transaction_id'
87 |
88 | result.secure_acceptance.secure_acceptance_url
89 | # e.g. https://www.paypal.com/cgibin/webscr?cmd=_express-checkout&token=EC-4WL17777V4111184H
90 |
91 | # (optional) get shipping information for order
92 | details = authorize.Transaction.details('transaction_id')
93 |
94 | authorize.Transaction.auth_continue('transaction_id', 'payer_id')
95 |
96 | authorize.Transaction.void('transaction_id')
97 |
98 |
99 | Transaction Flow Sequence Example 3
100 | -----------------------------------
101 |
102 | #. Authorization and Capture
103 | #. Get Details (recommended for shipping)
104 | #. Authorization and Capture, Continue
105 | #. Refund (optional)
106 |
107 | .. code-block:: python
108 |
109 | result = authorize.Transaction.sale({
110 | 'amount': 40.00,
111 | 'pay_pal': {
112 | 'success_url': 'https://my.server.com/success.html',
113 | 'cancel_url': 'https://my.server.com/cancel.html',
114 | 'locale': 'US',
115 | 'header_image': 'https://usa.visa.com/img/home/logo_visa.gif',
116 | 'flow_color': 'FF0000'
117 | },
118 | })
119 |
120 | result.transaction_response.trans_id
121 | # e.g. 'transaction_id'
122 |
123 | result.secure_acceptance.secure_acceptance_url
124 | # e.g. https://www.paypal.com/cgibin/webscr?cmd=_express-checkout&token=EC-4WL17777V4111184H
125 |
126 | # (optional) get shipping information for order
127 | details = authorize.Transaction.details('transaction_id')
128 |
129 | authorize.Transaction.sale_continue('transaction_id', 'payer_id')
130 |
131 | authorize.Transaction.refund('transaction_id')
132 |
133 |
--------------------------------------------------------------------------------
/docs/customer.rst:
--------------------------------------------------------------------------------
1 | Customer
2 | ========
3 |
4 | The ``Customer`` class provides an interface to Authorize.net's Customer
5 | Information Manager (CIM) API.
6 |
7 | Create
8 | ------
9 |
10 | When creating a customer profile, no information is actually needed. A random
11 | merchant ID is associated to the customer if none is provided. Once a user
12 | been created, address and payment information can then be associated to the
13 | profile.
14 |
15 | Minimal Example
16 | ~~~~~~~~~~~~~~~
17 |
18 | .. code-block:: python
19 |
20 | result = authorize.Customer.create()
21 |
22 | result.customer_id
23 | # e.g. '19086351'
24 |
25 |
26 | Creating Profile with Basic Information
27 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
28 |
29 | .. code-block:: python
30 |
31 | result = authorize.Customer.create({
32 | 'email': 'rob@robotronstudios.com',
33 | 'description': 'Rob the robot',
34 | 'customer_type': 'individual',
35 | })
36 |
37 | result.customer_id
38 | # e.g. '19086352'
39 |
40 |
41 | Full Example
42 | ~~~~~~~~~~~~
43 |
44 | When creating a customer, additional shipping address and payment information
45 | can be provided as well.
46 |
47 | .. code-block:: python
48 |
49 | result = authorize.Customer.create({
50 | 'merchant_id': '8989762983402603',
51 | 'email': 'rob@robotronstudios.com',
52 | 'description': 'Rob the robot',
53 | 'customer_type': 'individual',
54 | 'billing': {
55 | 'first_name': 'Rob',
56 | 'last_name': 'Oteron',
57 | 'company': 'Robotron Studios',
58 | 'address': '101 Computer Street',
59 | 'city': 'Tucson',
60 | 'state': 'AZ',
61 | 'zip': '85704',
62 | 'country': 'US',
63 | 'phone_number': '520-123-4567',
64 | 'fax_number': '520-456-7890',
65 | },
66 | 'credit_card': {
67 | 'card_number': '4111111111111111',
68 | 'card_code': '456',
69 | 'expiration_month': '04',
70 | 'expiration_year': '2014',
71 | },
72 | 'shipping': {
73 | 'first_name': 'Rob',
74 | 'last_name': 'Oteron',
75 | 'company': 'Robotron Studios',
76 | 'address': '101 Computer Street',
77 | 'city': 'Tucson',
78 | 'state': 'AZ',
79 | 'zip': '85704',
80 | 'country': 'US',
81 | 'phone_number': '520-123-4567',
82 | 'fax_number': '520-456-7890',
83 | }
84 | })
85 |
86 | result.customer_id
87 | # e.g. '19086352'
88 |
89 |
90 | Creating Profile from a Transaction
91 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
92 |
93 | .. code-block:: python
94 |
95 | result = authorize.Transaction.auth({
96 | 'amount': 40.00,
97 | 'email': 'rob@robotronstudios.com',
98 | 'credit_card': {
99 | 'card_number': '4111111111111111',
100 | 'expiration_date': '04/2014',
101 | }
102 | })
103 |
104 | trans_id = result.transaction_response.trans_id
105 | # e.g. '2194343352'
106 |
107 | result = authorize.Customer.from_transaction(trans_id)
108 |
109 | result.profile.email
110 | # rob@robotronstudios.com
111 |
112 | result.customer_id
113 | # e.g. '19086352'
114 |
115 |
116 | Details
117 | -------
118 |
119 | The ``details`` method returns the information for a given customer profile
120 | based on the customer ID.
121 |
122 | The following information is returned in the result attribute dictionary:
123 |
124 | - ``profile.merchant_id``
125 | - ``profile.email``
126 | - ``profile.description``
127 | - ``profile.customer_type``
128 | - ``address_ids``
129 | - ``payment_ids``
130 |
131 | .. code-block:: python
132 |
133 | result = authorize.Customer.details('19086352')
134 |
135 |
136 | Update
137 | ------
138 |
139 | Customer profile information can be easily updated on the server.
140 |
141 | .. code-block:: python
142 |
143 | result = authorize.Customer.update('19086352', {
144 | 'email': 'rob@robotronstudios.com',
145 | 'description': 'Rob the robot',
146 | 'customer_type': 'individual',
147 | })
148 |
149 |
150 | Delete
151 | ------
152 |
153 | Deleting a customer will delete the customer profile along with all stored
154 | addresses and billing information.
155 |
156 | .. code-block:: python
157 |
158 | result = authorize.Customer.delete('19086352')
159 |
160 |
161 | List
162 | ----
163 |
164 | The ``list`` method returns a list of all customer profile IDs.
165 |
166 | .. code-block:: python
167 |
168 | result = authorize.Customer.list()
169 |
170 | result.profile_ids
171 | # e.g. ['16467005', '16467010', '16467092', '17556329']
172 |
173 |
--------------------------------------------------------------------------------
/authorize/apis/customer_api.py:
--------------------------------------------------------------------------------
1 | import xml.etree.cElementTree as E
2 |
3 | from authorize.apis.base_api import BaseAPI
4 | from authorize.schemas import CustomerBaseSchema
5 | from authorize.schemas import CreateCustomerSchema
6 | from authorize.xml_data import *
7 |
8 |
9 | class CustomerAPI(BaseAPI):
10 |
11 | def create(self, params={}):
12 | customer = self._deserialize(CreateCustomerSchema().bind(), params)
13 | return self.api._make_call(self._create_request(customer))
14 |
15 | def from_transaction(self, transaction_id, params={}):
16 | customer = None
17 | if 'customer' in params:
18 | customer = self._deserialize(CustomerBaseSchema().bind(), params['customer'])
19 |
20 | return self.api._make_call(self._from_transaction_request(transaction_id, customer))
21 |
22 | def get_transactions(self, customer_id, payment_id=None):
23 | return self.api._make_call(self._get_transactions(customer_id, payment_id))
24 |
25 | def details(self, customer_id):
26 | return self.api._make_call(self._details_request(customer_id))
27 |
28 | def update(self, customer_id, params={}):
29 | customer = self._deserialize(CustomerBaseSchema().bind(), params)
30 | return self.api._make_call(self._update_request(customer_id, customer))
31 |
32 | def delete(self, customer_id):
33 | return self.api._make_call(self._delete_request(customer_id))
34 |
35 | def list(self):
36 | return self.api._make_call(self.api._base_request('getCustomerProfileIdsRequest'))
37 |
38 | # The following methods generate the XML for the corresponding API calls.
39 | # This makes unit testing each of the calls easier.
40 | def _create_request(self, customer={}):
41 | request = self.api._base_request('createCustomerProfileRequest')
42 | profile = create_profile(customer)
43 |
44 | payment_profiles = E.Element('paymentProfiles')
45 |
46 | # We are only concerned about payment profile information if a
47 | # payment method is included
48 | if 'credit_card' in customer or 'bank_account' in customer:
49 | # Customer type
50 | if 'customer_type' in customer:
51 | E.SubElement(payment_profiles, 'customerType').text = customer['customer_type']
52 |
53 | # Customer billing information
54 | if 'billing' in customer:
55 | payment_profiles.append(create_address('billTo', customer['billing']))
56 |
57 | # Payment method
58 | payment = E.SubElement(payment_profiles, 'payment')
59 | if 'credit_card' in customer:
60 | payment.append(create_card(customer['credit_card']))
61 | else:
62 | payment.append(create_account(customer['bank_account']))
63 | profile.append(payment_profiles)
64 |
65 | # Customer shipping information
66 | if 'shipping' in customer:
67 | profile.append(create_address('shipToList', customer['shipping']))
68 |
69 | request.append(profile)
70 |
71 | return request
72 |
73 | def _from_transaction_request(self, transaction_id, customer=None):
74 | request = self.api._base_request('createCustomerProfileFromTransactionRequest')
75 | trans_id = E.Element('transId')
76 | trans_id.text = transaction_id
77 | request.append(trans_id)
78 |
79 | if customer:
80 | customer = create_customer(customer)
81 | request.append(customer)
82 |
83 | return request
84 |
85 | def _get_transactions(self, customer_id, payment_id):
86 | request = self.api._base_request('getTransactionListForCustomerRequest')
87 |
88 | customerProfileId = E.Element('customerProfileId')
89 | customerProfileId.text = customer_id
90 | request.append(customerProfileId)
91 |
92 | if payment_id:
93 | customerPaymentProfileId = E.Element('customerPaymentProfileId')
94 | customerPaymentProfileId.text = payment_id
95 | request.append(customerPaymentProfileId)
96 |
97 | return request
98 |
99 | def _details_request(self, customer_id):
100 | request = self.api._base_request('getCustomerProfileRequest')
101 | E.SubElement(request, 'customerProfileId').text = customer_id
102 | E.SubElement(request, 'unmaskExpirationDate').text = 'true'
103 | return request
104 |
105 | def _update_request(self, customer_id, customer):
106 | request = self.api._base_request('updateCustomerProfileRequest')
107 | profile = create_profile(customer)
108 | E.SubElement(profile, 'customerProfileId').text = customer_id
109 | request.append(profile)
110 | return request
111 |
112 | def _delete_request(self, customer_id):
113 | request = self.api._base_request('deleteCustomerProfileRequest')
114 | E.SubElement(request, 'customerProfileId').text = customer_id
115 | return request
116 |
--------------------------------------------------------------------------------
/docs/credit_card.rst:
--------------------------------------------------------------------------------
1 | Credit Cards
2 | ============
3 |
4 | Credit cards must be associated to a customer profile on the Authorize.net
5 | server. A credit card can be associated when a new customer is created, to
6 | see how this is handled refer to the :doc:`Customer API`
7 | documentation.
8 |
9 | Create
10 | ------
11 |
12 | To add a credit card to an existing user, the minimal amount of information
13 | required is the credit card number, the expiration date and the customer
14 | profile ID. The customer profile ID is passed as the first argument to the
15 | ``create`` method.
16 |
17 | Minimal Example
18 | ~~~~~~~~~~~~~~~
19 |
20 | .. code-block:: python
21 |
22 | result = authorize.CreditCard.create('customer_id', {
23 | 'card_number': '4111111111111111',
24 | 'expiration_date': '04/2014',
25 | })
26 |
27 | result.payment_id
28 | # e.g. '17633318'
29 |
30 | When creating a new credit card, the expiration month and date can be
31 | separate values.
32 |
33 | .. code-block:: python
34 |
35 | result = authorize.CreditCard.create('customer_id', {
36 | 'card_number': '4111111111111111',
37 | 'expiration_month': '04',
38 | 'expiration_year': '2014',
39 | })
40 |
41 | result.payment_id
42 | # e.g. '17633319'
43 |
44 |
45 | Full Example
46 | ~~~~~~~~~~~~
47 |
48 | When creating a new credit card, the billing address information can also be
49 | associated to the card.
50 |
51 | .. code-block:: python
52 |
53 | result = authorize.CreditCard.create('customer_id', {
54 | 'customer_type': 'individual',
55 | 'card_number': '4111111111111111',
56 | 'expiration_month': '04',
57 | 'expiration_year': '2014',
58 | 'card_code': '123',
59 | 'billing': {
60 | 'first_name': 'Rob',
61 | 'last_name': 'Oteron',
62 | 'company': 'Robotron Studios',
63 | 'address': '101 Computer Street',
64 | 'city': 'Tucson',
65 | 'state': 'AZ',
66 | 'zip': '85704',
67 | 'country': 'US',
68 | 'phone_number': '520-123-4567',
69 | 'fax_number': '520-456-7890',
70 | },
71 | })
72 |
73 | result.payment_id
74 | # e.g. '17633319'
75 |
76 |
77 | Details
78 | -------
79 |
80 | The ``details`` method returns the information for a given customer payment
81 | profile. This method takes both the customer profile ID and customer payment
82 | profile ID.
83 |
84 | The following information is returned in the result attribute dictionary:
85 |
86 | - ``payment_profile.payment_id``
87 | - ``payment_profile.customer_type``
88 | - ``payment_profile.payment.credit_card.card_number``
89 | - ``payment_profile.payment.credit_card.expiration_date``
90 | - ``payment_profile.bill_to.company``
91 | - ``payment_profile.bill_to.first_name``
92 | - ``payment_profile.bill_to.last_name``
93 | - ``payment_profile.bill_to.address``
94 | - ``payment_profile.bill_to.city``
95 | - ``payment_profile.bill_to.state``
96 | - ``payment_profile.bill_to.zip``
97 | - ``payment_profile.bill_to.country``
98 | - ``payment_profile.bill_to.phone_number``
99 | - ``payment_profile.bill_to.fax_number``
100 |
101 | .. code-block:: python
102 |
103 | result = authorize.CreditCard.details('customer_id', '17633319')
104 |
105 |
106 | Update
107 | ------
108 |
109 | The ``update`` method will update the credit card information for a given
110 | payment profile ID. The method requires the customer profile ID, the payment
111 | profile ID and the new credit card information.
112 |
113 | .. code-block:: python
114 |
115 | result = authorize.CreditCard.update('customer_id', '17633319', {
116 | 'customer_type': 'individual',
117 | 'card_number': '4111111111111111',
118 | 'expiration_month': '04',
119 | 'expiration_year': '2014',
120 | 'card_code': '123',
121 | 'billing': {
122 | 'first_name': 'Rob',
123 | 'last_name': 'Oteron',
124 | 'company': 'Robotron Studios',
125 | 'address': '101 Computer Street',
126 | 'city': 'Tucson',
127 | 'state': 'AZ',
128 | 'zip': '85704',
129 | 'country': 'US',
130 | 'phone_number': '520-123-4567',
131 | 'fax_number': '520-456-7890',
132 | },
133 | })
134 |
135 |
136 | Delete
137 | ------
138 |
139 | Deleting a customer credit card will remove the payment profile from the
140 | given customer.
141 |
142 | .. code-block:: python
143 |
144 | result = authorize.CreditCard.delete('customer_id', '17633319')
145 |
146 |
147 | Validate
148 | --------
149 |
150 | Stored credit cards can be validated before attempting to run a transaction
151 | against them.
152 |
153 | .. code-block:: python
154 |
155 | result = authorize.CreditCard.validate('customer_id', '17633319', {
156 | 'card_code': '123',
157 | 'validationMode': 'liveMode'
158 | })
159 |
160 |
161 | Transactions
162 | ------------
163 |
164 | For information on how to run transactions agains stored credit cards,
165 | please refer to the :doc:`Transaction ` documentation.
166 |
--------------------------------------------------------------------------------
/docs/bank_account.rst:
--------------------------------------------------------------------------------
1 | Bank Accounts
2 | =============
3 |
4 | Bank accounts must be associated to a customer profile on the Authorize.net
5 | server. A bank account can be associated when a new customer is created, to
6 | see how this is handled refer to the :doc:`Customer API`
7 | documentation.
8 |
9 | .. note::
10 |
11 | The ability to process transactions from a bank account is not a
12 | standard gateway account feature. You must register for eCheck
13 | functionality seperately. For more information see `Authorize.net's
14 | eCheck documentation`_.
15 |
16 | .. _Authorize.net's eCheck documentation: http://www.authorize.net/solutions/merchantsolutions/merchantservices/echeck/
17 |
18 |
19 | Create
20 | ------
21 |
22 | To add a bank account to an existing user, the minimal amount of information
23 | required is the routing number, account number, name on the account and the
24 | customer profile ID. The customer profile ID is passed as the first argument
25 | to the ``create`` method.
26 |
27 | Minimal Example
28 | ~~~~~~~~~~~~~~~
29 |
30 | .. code-block:: python
31 |
32 | result = authorize.BankAccount.create('customer_id', {
33 | 'routing_number': '322271627',
34 | 'account_number': '00987467838473',
35 | 'name_on_account': 'Rob Otron',
36 | })
37 |
38 | result.payment_id
39 | # e.g. '17633593'
40 |
41 |
42 | Full Example
43 | ~~~~~~~~~~~~
44 |
45 | When creating a new bank account, the billing address information can also
46 | be associated to the account.
47 |
48 | .. code-block:: python
49 |
50 | result = authorize.BankAccount.create('customer_id', {
51 | 'customer_type': 'individual',
52 | 'account_type': 'checking',
53 | 'routing_number': '322271627',
54 | 'account_number': '00987467838473',
55 | 'name_on_account': 'Rob Otron',
56 | 'bank_name': 'Evil Bank Co.',
57 | 'echeck_type': 'CCD',
58 | 'billing': {
59 | 'first_name': 'Rob',
60 | 'last_name': 'Oteron',
61 | 'company': 'Robotron Studios',
62 | 'address': '101 Computer Street',
63 | 'city': 'Tucson',
64 | 'state': 'AZ',
65 | 'zip': '85704',
66 | 'country': 'US',
67 | 'phone_number': '520-123-4567',
68 | 'fax_number': '520-456-7890',
69 | },
70 | })
71 |
72 | result.payment_id
73 | # e.g. '17633614'
74 |
75 |
76 | Details
77 | -------
78 |
79 | The ``details`` method returns the information for a given customer payment
80 | profile. This method takes both the customer profile ID and customer payment
81 | profile ID.
82 |
83 | The following information is returned in the result attribute dictionary:
84 |
85 | - ``payment_profile.payment_id``
86 | - ``payment_profile.customer_type``
87 | - ``payment_profile.payment.bank_account.account_type``
88 | - ``payment_profile.payment.bank_account.routin_number``
89 | - ``payment_profile.payment.bank_account.account_number``
90 | - ``payment_profile.payment.bank_account.name_on_account``
91 | - ``payment_profile.payment.bank_account.bank_name``
92 | - ``payment_profile.payment.bank_account.echeck_type``
93 | - ``payment_profile.bill_to.company``
94 | - ``payment_profile.bill_to.first_name``
95 | - ``payment_profile.bill_to.last_name``
96 | - ``payment_profile.bill_to.address``
97 | - ``payment_profile.bill_to.city``
98 | - ``payment_profile.bill_to.state``
99 | - ``payment_profile.bill_to.zip``
100 | - ``payment_profile.bill_to.country``
101 | - ``payment_profile.bill_to.phone_number``
102 | - ``payment_profile.bill_to.fax_number``
103 |
104 | .. code-block:: python
105 |
106 | result = authorize.BankAccount.details('customer_id', '17633614')
107 |
108 |
109 | Update
110 | ------
111 |
112 | The ``update`` method will update the bank account information for a given
113 | payment profile ID. The method requires the customer profile ID, the payment
114 | profile ID and the new bank account information.
115 |
116 | .. code-block:: python
117 |
118 | result = authorize.BankAccount.update('customer_id', '17633614', {
119 | 'customer_type': 'individual',
120 | 'account_type': 'checking',
121 | 'routing_number': '322271627',
122 | 'account_number': '00987467838473',
123 | 'name_on_account': 'Rob Otron',
124 | 'bank_name': 'Evil Bank Co.',
125 | 'echeck_type': 'CCD',
126 | 'billing': {
127 | 'first_name': 'Rob',
128 | 'last_name': 'Oteron',
129 | 'company': 'Robotron Studios',
130 | 'address': '101 Computer Street',
131 | 'city': 'Tucson',
132 | 'state': 'AZ',
133 | 'zip': '85704',
134 | 'country': 'US',
135 | 'phone_number': '520-123-4567',
136 | 'fax_number': '520-456-7890',
137 | },
138 | })
139 |
140 |
141 | Delete
142 | ------
143 |
144 | Deleting a customer bank account will remove the payment profile from the
145 | given customer.
146 |
147 | .. code-block:: python
148 |
149 | result = authorize.BankAccount.delete('customer_id', '17633319')
150 |
151 |
152 | Transactions
153 | ------------
154 |
155 | For information on how to run transactions against stored credit cards,
156 | please refer to the :doc:`Transaction ` documentation.
157 |
--------------------------------------------------------------------------------
/tests/test_live_recurring.py:
--------------------------------------------------------------------------------
1 | import random
2 |
3 | from authorize import Recurring
4 | from authorize import AuthorizeResponseError
5 |
6 | from datetime import date
7 |
8 | from nose.plugins.attrib import attr
9 |
10 | from unittest import TestCase
11 |
12 | BASIC_RECURRING = {
13 | 'interval_length': 14,
14 | 'interval_unit': 'days',
15 | 'credit_card': {
16 | 'card_number': '4111111111111111',
17 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
18 | },
19 | }
20 |
21 | FULL_RECURRING = {
22 | 'name': 'Ultimate Robot Supreme Plan',
23 | 'total_occurrences': 30,
24 | 'interval_length': 2,
25 | 'interval_unit': 'months',
26 | 'trial_amount': 30.00,
27 | 'trial_occurrences': 2,
28 | 'credit_card': {
29 | 'card_number': '4111111111111111',
30 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
31 | 'card_code': '456',
32 | },
33 | 'billing': {
34 | 'first_name': 'Rob',
35 | 'last_name': 'Oteron',
36 | 'company': 'Robotron Studios',
37 | 'address': '101 Computer Street',
38 | 'city': 'Tucson',
39 | 'state': 'AZ',
40 | 'zip': '85704',
41 | 'country': 'US',
42 | },
43 | 'shipping': {
44 | 'first_name': 'Rob',
45 | 'last_name': 'Oteron',
46 | 'company': 'Robotron Studios',
47 | 'address': '101 Computer Street',
48 | 'city': 'Tucson',
49 | 'state': 'AZ',
50 | 'zip': '85704',
51 | 'country': 'US',
52 | },
53 | 'order': {
54 | 'invoice_number': 'INV0001',
55 | 'description': 'Just another invoice...',
56 | },
57 | 'customer': {
58 | 'merchant_id': '1234567890',
59 | 'email': 'rob@robotronstudios.com',
60 | 'description': 'I am a robot',
61 | },
62 | }
63 |
64 | # When updating a subscription the only parameters we cannot update are the
65 | # interval unit and interval length
66 | UPDATE_RECURRING = {
67 | 'name': 'Ultimate Robot Supreme Plan',
68 | 'total_occurrences': 30,
69 | 'trial_amount': 30.00,
70 | 'trial_occurrences': 2,
71 | 'credit_card': {
72 | 'card_number': '4111111111111111',
73 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
74 | 'card_code': '456',
75 | },
76 | 'billing': {
77 | 'first_name': 'Rob',
78 | 'last_name': 'Oteron',
79 | 'company': 'Robotron Studios',
80 | 'address': '101 Computer Street',
81 | 'city': 'Tucson',
82 | 'state': 'AZ',
83 | 'zip': '85704',
84 | 'country': 'US',
85 | },
86 | 'shipping': {
87 | 'first_name': 'Rob',
88 | 'last_name': 'Oteron',
89 | 'company': 'Robotron Studios',
90 | 'address': '101 Computer Street',
91 | 'city': 'Tucson',
92 | 'state': 'AZ',
93 | 'zip': '85704',
94 | 'country': 'US',
95 | },
96 | 'order': {
97 | 'invoice_number': 'INV0001',
98 | 'description': 'Just another invoice...',
99 | },
100 | 'customer': {
101 | 'merchant_id': '1234567890',
102 | 'email': 'rob@robotronstudios.com',
103 | 'description': 'I am a robot',
104 | },
105 | }
106 |
107 | UPDATE_RECURRING_PAYMENT_ONLY = {
108 | 'credit_card': {
109 | 'card_number': '4111111111111111',
110 | 'expiration_date': '04/{0}'.format(date.today().year + 1),
111 | 'card_code': '456',
112 | },
113 | }
114 |
115 |
116 | @attr('live_tests')
117 | class RecurringTests(TestCase):
118 |
119 | def test_live_recurring(self):
120 | # Create a new recurring subscription. The amount needs to be random,
121 | # otherwise the subscription will register as a duplicate
122 | recurring = FULL_RECURRING.copy()
123 | recurring['amount'] = random.randrange(100, 100000) / 100.0
124 | Recurring.create(recurring)
125 |
126 | # An error will occur if we attempt to create a duplicate
127 | # subscription
128 | self.assertRaises(AuthorizeResponseError, Recurring.create, recurring)
129 |
130 | recurring = BASIC_RECURRING.copy()
131 | recurring['amount'] = random.randrange(100, 100000) / 100.0
132 | result = Recurring.create(recurring)
133 | subscription_id = result.subscription_id
134 |
135 | Recurring.details(subscription_id)
136 |
137 | Recurring.status(subscription_id)
138 |
139 | # Update subscription information
140 | recurring = UPDATE_RECURRING.copy()
141 | recurring['amount'] = random.randrange(100, 100000) / 100.0
142 | Recurring.update(subscription_id, recurring)
143 |
144 | # Update only credit card information
145 | Recurring.update(subscription_id, UPDATE_RECURRING_PAYMENT_ONLY)
146 |
147 | # Update without credit card (or bank account) information
148 | recurring_update = UPDATE_RECURRING.copy()
149 | del recurring_update['credit_card']
150 | Recurring.update(subscription_id, recurring_update)
151 |
152 | # Cancel (delete) the subscription
153 | Recurring.delete(subscription_id)
154 |
155 | # Issue 26: Make sure we don't update the start date for
156 | # subscriptions with at least one transaction
157 | recurring = BASIC_RECURRING.copy()
158 | recurring['amount'] = random.randrange(100, 100000) / 100.0
159 | result = Recurring.update('1666555', recurring)
160 |
161 | # Test paging recurring payments
162 | params = {
163 | 'search_type': 'subscriptionActive',
164 | 'sorting': {
165 | 'order_by': 'accountNumber'
166 | },
167 | 'paging': {
168 | 'offset': 1,
169 | 'limit': 1000
170 | }
171 | }
172 | result = Recurring.list(params)
173 |
--------------------------------------------------------------------------------
/tests/test_bank_account_api.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 | from authorize.xml_data import prettify
3 |
4 | from unittest import TestCase
5 |
6 | CREATE_BANK_ACCOUNT = {
7 | 'customer_type': 'individual',
8 | 'account_type': 'checking',
9 | 'routing_number': '322271627',
10 | 'account_number': '00987467838473',
11 | 'name_on_account': 'Rob Otron',
12 | 'bank_name': 'Evil Bank Co.',
13 | 'echeck_type': 'CCD',
14 | 'billing': {
15 | 'first_name': 'Rob',
16 | 'last_name': 'Oteron',
17 | 'company': 'Robotron Studios',
18 | 'address': '101 Computer Street',
19 | 'city': 'Tucson',
20 | 'state': 'AZ',
21 | 'zip': '85704',
22 | 'country': 'US',
23 | 'phone_number': '520-123-4567',
24 | 'fax_number': '520-456-7890',
25 | },
26 | }
27 |
28 | UPDATE_BANK_ACCOUNT = {
29 | 'customer_type': 'individual',
30 | 'account_type': 'checking',
31 | 'routing_number': '322271627',
32 | 'account_number': '00987467838473',
33 | 'name_on_account': 'Rob Otron',
34 | 'bank_name': 'Evil Bank Co.',
35 | 'echeck_type': 'CCD',
36 | 'billing': {
37 | 'first_name': 'Rob',
38 | 'last_name': 'Oteron',
39 | 'company': 'Robotron Studios',
40 | 'address': '101 Computer Street',
41 | 'city': 'Tucson',
42 | 'state': 'AZ',
43 | 'zip': '85704',
44 | 'country': 'US',
45 | 'phone_number': '520-123-4567',
46 | 'fax_number': '520-456-7890',
47 | },
48 | }
49 |
50 | CREATE_BANK_ACCOUNT_REQUEST = '''
51 |
52 |
53 |
54 | 8s8tVnG5t
55 | 5GK7mncw8mG2946z
56 |
57 | 1234567890
58 |
59 | individual
60 |
61 | Rob
62 | Oteron
63 | Robotron Studios
64 | 101 Computer Street
65 | Tucson
66 | AZ
67 | 85704
68 | US
69 | 520-123-4567
70 | 520-456-7890
71 |
72 |
73 |
74 | checking
75 | 322271627
76 | 00987467838473
77 | Rob Otron
78 | CCD
79 | Evil Bank Co.
80 |
81 |
82 |
83 | '''
84 |
85 | DETAILS_BANK_ACCOUNT_REQUEST = '''
86 |
87 |
88 |
89 | 8s8tVnG5t
90 | 5GK7mncw8mG2946z
91 |
92 | 1234567890
93 | 0987654321
94 | true
95 | '''
96 |
97 | UPDATE_BANK_ACCOUNT_REQUEST = '''
98 |
99 |
100 |
101 | 8s8tVnG5t
102 | 5GK7mncw8mG2946z
103 |
104 | 1234567890
105 |
106 | individual
107 |
108 | Rob
109 | Oteron
110 | Robotron Studios
111 | 101 Computer Street
112 | Tucson
113 | AZ
114 | 85704
115 | US
116 | 520-123-4567
117 | 520-456-7890
118 |
119 |
120 |
121 | checking
122 | 322271627
123 | 00987467838473
124 | Rob Otron
125 | CCD
126 | Evil Bank Co.
127 |
128 |
129 | 0987654321
130 |
131 | '''
132 |
133 | DELETE_BANK_ACCOUNT_REQUEST = '''
134 |
135 |
136 |
137 | 8s8tVnG5t
138 | 5GK7mncw8mG2946z
139 |
140 | 1234567890
141 | 0987654321
142 | '''
143 |
144 |
145 | class BankAccountAPITests(TestCase):
146 |
147 | maxDiff = None
148 |
149 | def test_create_bank_account_request(self):
150 | request_xml = Configuration.api.bank_account._create_request('1234567890', CREATE_BANK_ACCOUNT)
151 | request_string = prettify(request_xml)
152 | self.assertEqual(request_string, CREATE_BANK_ACCOUNT_REQUEST.strip())
153 |
154 | def test_details_bank_account_request(self):
155 | request_xml = Configuration.api.bank_account._details_request('1234567890', '0987654321')
156 | request_string = prettify(request_xml)
157 | self.assertEqual(request_string, DETAILS_BANK_ACCOUNT_REQUEST.strip())
158 |
159 | def test_update_bank_account_request(self):
160 | request_xml = Configuration.api.bank_account._update_request('1234567890', '0987654321', UPDATE_BANK_ACCOUNT)
161 | request_string = prettify(request_xml)
162 | self.assertEqual(request_string, UPDATE_BANK_ACCOUNT_REQUEST.strip())
163 |
164 | def test_delete_bank_account_request(self):
165 | request_xml = Configuration.api.bank_account._delete_request('1234567890', '0987654321')
166 | request_string = prettify(request_xml)
167 | self.assertEqual(request_string, DELETE_BANK_ACCOUNT_REQUEST.strip())
168 |
--------------------------------------------------------------------------------
/tests/test_xml_data.py:
--------------------------------------------------------------------------------
1 | from authorize.xml_data import *
2 | from unittest import TestCase
3 |
4 | PROFILE = {
5 | 'merchant_id': '1234567890',
6 | 'email': 'rob@robotronstudios.com',
7 | 'description': 'I am a robot',
8 | }
9 |
10 | ADDRESS = {
11 | 'first_name': 'Rob',
12 | 'last_name': 'Oteron',
13 | 'company': 'Robotron Studios',
14 | 'address': '101 Computer Street',
15 | 'city': 'Tucson',
16 | 'state': 'AZ',
17 | 'zip': '85704',
18 | 'country': 'US',
19 | 'phone_number': '520-123-4567',
20 | 'fax_number': '520-456-7890',
21 | }
22 |
23 | CREDIT_CARD = {
24 | 'card_number': '4111111111111111',
25 | 'expiration_year': '2014',
26 | 'expiration_month': '04',
27 | 'card_code': '343',
28 | }
29 |
30 | BANK_ACCOUNT = {
31 | 'account_type': 'checking',
32 | 'routing_number': '322271627',
33 | 'account_number': '00987467838473',
34 | 'name_on_account': 'Rob Otron',
35 | 'bank_name': 'Evil Bank Co.',
36 | 'echeck_type': 'WEB',
37 | }
38 |
39 | LINE_ITEM = {
40 | 'item_id': 'CIR0001',
41 | 'name': 'Circuit Board',
42 | 'description': 'A brand new robot component',
43 | 'quantity': 5,
44 | 'unit_price': 99.99,
45 | 'taxable': True,
46 | }
47 |
48 | LINE_ITEMS = [{
49 | 'item_id': 'CIR0001',
50 | 'name': 'Circuit Board',
51 | 'description': 'A brand new robot component',
52 | 'quantity': 5,
53 | 'unit_price': 99.99,
54 | 'taxable': True,
55 | }, {
56 | 'item_id': 'CIR0002',
57 | 'name': 'Circuit Board 2.0',
58 | 'description': 'Another new robot component',
59 | 'quantity': 1,
60 | 'unit_price': 86.99,
61 | 'taxable': True,
62 | }, {
63 | 'item_id': 'SCRDRVR',
64 | 'name': 'Screwdriver',
65 | 'description': 'A basic screwdriver',
66 | 'quantity': 1,
67 | 'unit_price': 10.00,
68 | 'taxable': False,
69 | }]
70 |
71 | TAX_AMOUNT_TYPE = {
72 | 'amount': 45.00,
73 | 'name': 'Double Taxation Tax',
74 | 'description': 'Another tax for paying double tax',
75 | }
76 |
77 | ORDER = {
78 | 'invoice_number': 'INV0001',
79 | 'description': 'Just another invoice...',
80 | }
81 |
82 | CREATE_PROFILE_XML = '''
83 |
84 |
85 | 1234567890
86 | I am a robot
87 | rob@robotronstudios.com
88 |
89 | '''
90 |
91 | CREATE_ADDRESS_XML = '''
92 |
93 |
94 | Rob
95 | Oteron
96 | Robotron Studios
97 | 101 Computer Street
98 | Tucson
99 | AZ
100 | 85704
101 | US
102 | 520-123-4567
103 | 520-456-7890
104 |
105 | '''
106 |
107 | CREATE_CARD_XML = '''
108 |
109 |
110 | 4111111111111111
111 | 2014-04
112 | 343
113 |
114 | '''
115 |
116 | CREATE_ACCOUNT_XML = '''
117 |
118 |
119 | checking
120 | 322271627
121 | 00987467838473
122 | Rob Otron
123 | WEB
124 | Evil Bank Co.
125 |
126 | '''
127 |
128 | CREATE_LINE_ITEM_XML = '''
129 |
130 |
131 | CIR0001
132 | Circuit Board
133 | A brand new robot component
134 | 5
135 | 99.99
136 | true
137 |
138 | '''
139 |
140 | CREATE_LINE_ITEMS_XML = '''
141 |
142 |
143 |
144 | CIR0001
145 | Circuit Board
146 | A brand new robot component
147 | 5
148 | 99.99
149 | true
150 |
151 |
152 | CIR0002
153 | Circuit Board 2.0
154 | Another new robot component
155 | 1
156 | 86.99
157 | true
158 |
159 |
160 | SCRDRVR
161 | Screwdriver
162 | A basic screwdriver
163 | 1
164 | 10.00
165 | false
166 |
167 |
168 | '''
169 |
170 | CREATE_AMOUNT_TYPE_XML = '''
171 |
172 |
173 | 45.00
174 | Double Taxation Tax
175 | Another tax for paying double tax
176 |
177 | '''
178 |
179 | CREATE_ORDER_XML = '''
180 |
181 |
182 | INV0001
183 | Just another invoice...
184 |
185 | '''
186 |
187 |
188 | class XMLDataTests(TestCase):
189 |
190 | maxDiff = None
191 |
192 | def test_create_profile(self):
193 | profile_xml = create_profile(PROFILE)
194 | profile_string = prettify(profile_xml)
195 | self.assertEqual(profile_string, CREATE_PROFILE_XML.strip())
196 |
197 | def test_create_address(self):
198 | address_xml = create_address('billTo', ADDRESS)
199 | address_string = prettify(address_xml)
200 | self.assertEqual(address_string, CREATE_ADDRESS_XML.strip())
201 |
202 | def test_create_card(self):
203 | card_xml = create_card(CREDIT_CARD)
204 | card_string = prettify(card_xml)
205 | self.assertEqual(card_string, CREATE_CARD_XML.strip())
206 |
207 | def test_create_account(self):
208 | account_xml = create_account(BANK_ACCOUNT)
209 | account_string = prettify(account_xml)
210 | self.assertEqual(account_string, CREATE_ACCOUNT_XML.strip())
211 |
212 | def test_create_line_item(self):
213 | line_item_xml = create_line_item('lineItem', LINE_ITEM)
214 | line_item_string = prettify(line_item_xml)
215 | self.assertEqual(line_item_string, CREATE_LINE_ITEM_XML.strip())
216 |
217 | def test_create_line_items(self):
218 | line_items_xml = create_line_items(LINE_ITEMS)
219 | line_items_string = prettify(line_items_xml)
220 | self.assertEqual(line_items_string, CREATE_LINE_ITEMS_XML.strip())
221 |
222 | def test_create_amount_type(self):
223 | tax_amount_type_xml = create_amount_type('tax', TAX_AMOUNT_TYPE)
224 | tax_amount_type_string = prettify(tax_amount_type_xml)
225 | self.assertEqual(tax_amount_type_string, CREATE_AMOUNT_TYPE_XML.strip())
226 |
227 | def test_create_order(self):
228 | order_xml = create_order(ORDER)
229 | order_string = prettify(order_xml)
230 | self.assertEqual(order_string, CREATE_ORDER_XML.strip())
231 |
--------------------------------------------------------------------------------
/docs/recurring.rst:
--------------------------------------------------------------------------------
1 | Recurring
2 | =========
3 |
4 | The Py-Authorize Recurring API is used to integrate with Authorize.net's
5 | Automated Recurring Billing (ARB) subscription-based payment service. It
6 | provides all functionality for managing recurring billing against credit
7 | cards and bank accounts.
8 |
9 | Create
10 | ------
11 |
12 | Authorize.net's ARB service functions seperately from the Customer
13 | Information Management API. This means you cannot create recurring payments
14 | for stored customers, credit cards or bank accounts. Instead, you will need
15 | to provide all customer and payment information explicitly.
16 |
17 | Minimal Example
18 | ~~~~~~~~~~~~~~~
19 |
20 | .. code-block:: python
21 |
22 | result = authorize.Recurring.create({
23 | 'amount': 45.00,
24 | 'interval_length': 1,
25 | 'interval_unit': 'months',
26 | 'credit_card': {
27 | 'card_number': '4111111111111111',
28 | 'expiration_date': '04-2014',
29 | 'card_code': '456',
30 | },
31 | })
32 |
33 | # result.subscription_id
34 | # e.g. '1725604'
35 |
36 |
37 | In this example, the customer will be charged $45.00 every month until the
38 | subscription is canceled or the payment gateway can no longer process the
39 | payment method (e.g. the card has expired). Authorize.net only permits
40 | interval units of `days` or `years`.
41 |
42 | To specify a limited number of occurrences, use the `total_occurrences`
43 | parameter.
44 |
45 | .. code-block:: python
46 |
47 | result = authorize.Recurring.create({
48 | 'amount': 45.00,
49 | 'interval_length': 14,
50 | 'interval_unit': 'days',
51 | 'total_occurrences': 52,
52 | 'credit_card': {
53 | 'card_number': '4111111111111111',
54 | 'expiration_date': '04-2014',
55 | 'card_code': '456',
56 | },
57 | })
58 |
59 | # result.subscription_id
60 | # e.g. '1725605'
61 |
62 |
63 | Full Example
64 | ~~~~~~~~~~~~
65 |
66 | Recurring payments can also be configured with customer bank accounts. The
67 | following example shows all recurring payment parameters available.
68 |
69 | .. code-block:: python
70 |
71 | result = authorize.Recurring.create({
72 | 'amount': 45.00,
73 | 'name': 'Ultimate Robot Supreme Plan',
74 | 'total_occurrences': 30,
75 | 'interval_length': 2,
76 | 'interval_unit': 'months',
77 | 'trial_amount': 30.00,
78 | 'trial_occurrences': 2,
79 | 'bank_account': {
80 | 'customer_type': 'individual',
81 | 'account_type': 'checking',
82 | 'routing_number': '322271627',
83 | 'account_number': '00987467838473',
84 | 'name_on_account': 'Rob Otron',
85 | 'bank_name': 'Evil Bank Co.',
86 | },
87 | 'billing': {
88 | 'first_name': 'Rob',
89 | 'last_name': 'Oteron',
90 | 'company': 'Robotron Studios',
91 | 'address': '101 Computer Street',
92 | 'city': 'Tucson',
93 | 'state': 'AZ',
94 | 'zip': '85704',
95 | 'country': 'US',
96 | },
97 | 'shipping': {
98 | 'first_name': 'Rob',
99 | 'last_name': 'Oteron',
100 | 'company': 'Robotron Studios',
101 | 'address': '101 Computer Street',
102 | 'city': 'Tucson',
103 | 'state': 'AZ',
104 | 'zip': '85704',
105 | 'country': 'US',
106 | },
107 | 'order': {
108 | 'invoice_number': 'INV0001',
109 | 'description': 'Just another invoice...',
110 | },
111 | 'customer': {
112 | 'merchant_id': '1234567890',
113 | 'email': 'rob@robotronstudios.com',
114 | 'description': 'I am a robot',
115 | },
116 | })
117 |
118 | # result.subscription_id
119 | # e.g. '1725628'
120 |
121 |
122 | Details
123 | -------
124 |
125 | To the get the details of a recurring payment, use the `details` method.
126 |
127 | .. code-block:: python
128 |
129 | result = authorize.Recurring.details('1725628')
130 |
131 | # result.subscription.profile.customer_id
132 | # e.g. '1806948040'
133 |
134 | # result.status
135 | # e.g. 'active'
136 |
137 |
138 | Status
139 | -------
140 |
141 | To the get the status of a recurring payment, use the `status` method.
142 |
143 | .. code-block:: python
144 |
145 | result = authorize.Recurring.status('1725628')
146 |
147 | # result.status
148 | # e.g. 'active'
149 |
150 |
151 | Update
152 | ------
153 |
154 | The `update` method takes the same parameters as the `create` method.
155 | However, once recurring payments have started, there are certain exceptions.
156 |
157 | - The subscription `start_date` may only be updated if no successful
158 | payments have been completed.
159 | - The subscription `interval_length` and `interval_unit` may not be updated.
160 | Instead, you must create a new subscription if you want different values
161 | for these parameters.
162 | - The number of `trial_occurrences` may only be updated if the subscription
163 | has not yet begun or is still in the trial period.
164 | - If the `start_date` is the 31st, and the interval is monthly, the billing
165 | date is the last day of each month (even when the month does not have 31
166 | days).
167 |
168 |
169 | When updating a recurring payment, you must pass in the subscription ID of
170 | the payment you wish to update along with the new subscription information.
171 |
172 | .. code-block:: python
173 |
174 | result = authorize.Recurring.update('1725628', {
175 | 'name': 'Ultimate Robot Supreme Plan',
176 | 'amount': 45.00,
177 | 'total_occurrences': 30,
178 | 'trial_amount': 30.00,
179 | 'trial_occurrences': 2,
180 | 'credit_card': {
181 | 'card_number': '4111111111111111',
182 | 'expiration_date': '04-2014',
183 | 'card_code': '456',
184 | },
185 | 'billing': {
186 | 'first_name': 'Rob',
187 | 'last_name': 'Oteron',
188 | 'company': 'Robotron Studios',
189 | 'address': '101 Computer Street',
190 | 'city': 'Tucson',
191 | 'state': 'AZ',
192 | 'zip': '85704',
193 | 'country': 'US',
194 | },
195 | 'shipping': {
196 | 'first_name': 'Rob',
197 | 'last_name': 'Oteron',
198 | 'company': 'Robotron Studios',
199 | 'address': '101 Computer Street',
200 | 'city': 'Tucson',
201 | 'state': 'AZ',
202 | 'zip': '85704',
203 | 'country': 'US',
204 | },
205 | 'order': {
206 | 'invoice_number': 'INV0001',
207 | 'description': 'Just another invoice...',
208 | },
209 | 'customer': {
210 | 'merchant_id': '1234567890',
211 | 'email': 'rob@robotronstudios.com',
212 | 'description': 'I am a robot',
213 | },
214 | })
215 |
216 |
217 | Cancel
218 | ------
219 |
220 | To cancel a recurring payment, pass the subscription ID to the `cancel`
221 | method.
222 |
223 | .. code-block:: python
224 |
225 | authorize.Recurring.delete('1725628')
226 |
227 |
228 |
229 |
230 |
231 |
--------------------------------------------------------------------------------
/tests/test_customer_api.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 | from authorize.xml_data import prettify
3 |
4 | from unittest import TestCase
5 |
6 |
7 | CREATE_CUSTOMER = {
8 | 'merchant_id': '1234567890',
9 | 'email': 'rob@robotronstudios.com',
10 | 'description': 'I am a robot',
11 | 'customer_type': 'individual',
12 | 'billing': {
13 | 'first_name': 'Rob',
14 | 'last_name': 'Oteron',
15 | 'company': 'Robotron Studios',
16 | 'address': '101 Computer Street',
17 | 'city': 'Tucson',
18 | 'state': 'AZ',
19 | 'zip': '85704',
20 | 'country': 'US',
21 | 'phone_number': '520-123-4567',
22 | 'fax_number': '520-456-7890',
23 | },
24 | 'bank_account': {
25 | 'account_type': 'checking',
26 | 'routing_number': '322271627',
27 | 'account_number': '00987467838473',
28 | 'name_on_account': 'Rob Otron',
29 | 'bank_name': 'Evil Bank Co.',
30 | 'echeck_type': 'CCD'
31 | },
32 | 'shipping': {
33 | 'first_name': 'Rob',
34 | 'last_name': 'Oteron',
35 | 'company': 'Robotron Studios',
36 | 'address': '101 Computer Street',
37 | 'city': 'Tucson',
38 | 'state': 'AZ',
39 | 'zip': '85704',
40 | 'country': 'US',
41 | },
42 | }
43 |
44 | CUSTOMER_FROM_TRANSACTION = {
45 | 'merchant_id': '1234567890',
46 | 'email': 'rob@robotronstudios.com',
47 | 'description': 'I am a robot',
48 | }
49 |
50 | UPDATE_CUSTOMER = {
51 | 'merchant_id': '1234567890',
52 | 'email': 'rob@robotronstudios.com',
53 | 'description': 'I am a robot',
54 | 'customer_type': 'individual',
55 | }
56 |
57 | CREATE_CUSTOMER_REQUEST = '''
58 |
59 |
60 |
61 | 8s8tVnG5t
62 | 5GK7mncw8mG2946z
63 |
64 |
65 | 1234567890
66 | I am a robot
67 | rob@robotronstudios.com
68 |
69 | individual
70 |
71 | Rob
72 | Oteron
73 | Robotron Studios
74 | 101 Computer Street
75 | Tucson
76 | AZ
77 | 85704
78 | US
79 | 520-123-4567
80 | 520-456-7890
81 |
82 |
83 |
84 | checking
85 | 322271627
86 | 00987467838473
87 | Rob Otron
88 | CCD
89 | Evil Bank Co.
90 |
91 |
92 |
93 |
94 | Rob
95 | Oteron
96 | Robotron Studios
97 | 101 Computer Street
98 | Tucson
99 | AZ
100 | 85704
101 | US
102 |
103 |
104 |
105 | '''
106 |
107 | CUSTOMER_FROM_TRANSACTION_REQUEST = '''
108 |
109 |
110 |
111 | 8s8tVnG5t
112 | 5GK7mncw8mG2946z
113 |
114 | 123456
115 |
116 | '''
117 |
118 | CUSTOMER_FROM_TRANSACTION_FULL_REQUEST = '''
119 |
120 |
121 |
122 | 8s8tVnG5t
123 | 5GK7mncw8mG2946z
124 |
125 | 123456
126 |
127 | 1234567890
128 | I am a robot
129 | rob@robotronstudios.com
130 |
131 |
132 | '''
133 |
134 | CUSTOMER_DETAILS_REQUEST = '''
135 |
136 |
137 |
138 | 8s8tVnG5t
139 | 5GK7mncw8mG2946z
140 |
141 | 1234567890
142 | true
143 |
144 | '''
145 |
146 | CUSTOMER_UPDATE_REQUEST = '''
147 |
148 |
149 |
150 | 8s8tVnG5t
151 | 5GK7mncw8mG2946z
152 |
153 |
154 | 1234567890
155 | I am a robot
156 | rob@robotronstudios.com
157 | 1234567890
158 |
159 |
160 | '''
161 |
162 | CUSTOMER_DELETE_REQUEST = '''
163 |
164 |
165 |
166 | 8s8tVnG5t
167 | 5GK7mncw8mG2946z
168 |
169 | 1234567890
170 |
171 | '''
172 |
173 |
174 | class CustomerAPITests(TestCase):
175 |
176 | maxDiff = None
177 |
178 | def test_create_customer_request(self):
179 | request_xml = Configuration.api.customer._create_request(CREATE_CUSTOMER)
180 | request_string = prettify(request_xml)
181 | self.assertEqual(request_string, CREATE_CUSTOMER_REQUEST.strip())
182 |
183 | def test_customer_from_transaction_request(self):
184 | request_xml = Configuration.api.customer._from_transaction_request('123456')
185 | request_string = prettify(request_xml)
186 | self.assertEqual(request_string, CUSTOMER_FROM_TRANSACTION_REQUEST.strip())
187 |
188 | def test_customer_from_transaction_full_request(self):
189 | request_xml = Configuration.api.customer._from_transaction_request('123456', CUSTOMER_FROM_TRANSACTION)
190 | request_string = prettify(request_xml)
191 | self.assertEqual(request_string, CUSTOMER_FROM_TRANSACTION_FULL_REQUEST.strip())
192 |
193 | def test_details_customer_request(self):
194 | request_xml = Configuration.api.customer._details_request('1234567890')
195 | request_string = prettify(request_xml)
196 | self.assertEqual(request_string, CUSTOMER_DETAILS_REQUEST.strip())
197 |
198 | def test_update_customer_request(self):
199 | request_xml = Configuration.api.customer._update_request('1234567890', UPDATE_CUSTOMER)
200 | request_string = prettify(request_xml)
201 | self.assertEqual(request_string, CUSTOMER_UPDATE_REQUEST.strip())
202 |
203 | def test_delete_customer_request(self):
204 | request_xml = Configuration.api.customer._delete_request('1234567890')
205 | request_string = prettify(request_xml)
206 | self.assertEqual(request_string, CUSTOMER_DELETE_REQUEST.strip())
207 |
--------------------------------------------------------------------------------
/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | REM Command file for Sphinx documentation
4 |
5 | if "%SPHINXBUILD%" == "" (
6 | set SPHINXBUILD=sphinx-build
7 | )
8 | set BUILDDIR=_build
9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
10 | set I18NSPHINXOPTS=%SPHINXOPTS% .
11 | if NOT "%PAPER%" == "" (
12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
14 | )
15 |
16 | if "%1" == "" goto help
17 |
18 | if "%1" == "help" (
19 | :help
20 | echo.Please use `make ^` where ^ is one of
21 | echo. html to make standalone HTML files
22 | echo. dirhtml to make HTML files named index.html in directories
23 | echo. singlehtml to make a single large HTML file
24 | echo. pickle to make pickle files
25 | echo. json to make JSON files
26 | echo. htmlhelp to make HTML files and a HTML help project
27 | echo. qthelp to make HTML files and a qthelp project
28 | echo. devhelp to make HTML files and a Devhelp project
29 | echo. epub to make an epub
30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
31 | echo. text to make text files
32 | echo. man to make manual pages
33 | echo. texinfo to make Texinfo files
34 | echo. gettext to make PO message catalogs
35 | echo. changes to make an overview over all changed/added/deprecated items
36 | echo. xml to make Docutils-native XML files
37 | echo. pseudoxml to make pseudoxml-XML files for display purposes
38 | echo. linkcheck to check all external links for integrity
39 | echo. doctest to run all doctests embedded in the documentation if enabled
40 | goto end
41 | )
42 |
43 | if "%1" == "clean" (
44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
45 | del /q /s %BUILDDIR%\*
46 | goto end
47 | )
48 |
49 |
50 | %SPHINXBUILD% 2> nul
51 | if errorlevel 9009 (
52 | echo.
53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
54 | echo.installed, then set the SPHINXBUILD environment variable to point
55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
56 | echo.may add the Sphinx directory to PATH.
57 | echo.
58 | echo.If you don't have Sphinx installed, grab it from
59 | echo.http://sphinx-doc.org/
60 | exit /b 1
61 | )
62 |
63 | if "%1" == "html" (
64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
65 | if errorlevel 1 exit /b 1
66 | echo.
67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html.
68 | goto end
69 | )
70 |
71 | if "%1" == "dirhtml" (
72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
73 | if errorlevel 1 exit /b 1
74 | echo.
75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
76 | goto end
77 | )
78 |
79 | if "%1" == "singlehtml" (
80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
81 | if errorlevel 1 exit /b 1
82 | echo.
83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
84 | goto end
85 | )
86 |
87 | if "%1" == "pickle" (
88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
89 | if errorlevel 1 exit /b 1
90 | echo.
91 | echo.Build finished; now you can process the pickle files.
92 | goto end
93 | )
94 |
95 | if "%1" == "json" (
96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
97 | if errorlevel 1 exit /b 1
98 | echo.
99 | echo.Build finished; now you can process the JSON files.
100 | goto end
101 | )
102 |
103 | if "%1" == "htmlhelp" (
104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
105 | if errorlevel 1 exit /b 1
106 | echo.
107 | echo.Build finished; now you can run HTML Help Workshop with the ^
108 | .hhp project file in %BUILDDIR%/htmlhelp.
109 | goto end
110 | )
111 |
112 | if "%1" == "qthelp" (
113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
114 | if errorlevel 1 exit /b 1
115 | echo.
116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^
117 | .qhcp project file in %BUILDDIR%/qthelp, like this:
118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Py-Authorize.qhcp
119 | echo.To view the help file:
120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Py-Authorize.ghc
121 | goto end
122 | )
123 |
124 | if "%1" == "devhelp" (
125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
126 | if errorlevel 1 exit /b 1
127 | echo.
128 | echo.Build finished.
129 | goto end
130 | )
131 |
132 | if "%1" == "epub" (
133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
134 | if errorlevel 1 exit /b 1
135 | echo.
136 | echo.Build finished. The epub file is in %BUILDDIR%/epub.
137 | goto end
138 | )
139 |
140 | if "%1" == "latex" (
141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
142 | if errorlevel 1 exit /b 1
143 | echo.
144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
145 | goto end
146 | )
147 |
148 | if "%1" == "latexpdf" (
149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
150 | cd %BUILDDIR%/latex
151 | make all-pdf
152 | cd %BUILDDIR%/..
153 | echo.
154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
155 | goto end
156 | )
157 |
158 | if "%1" == "latexpdfja" (
159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
160 | cd %BUILDDIR%/latex
161 | make all-pdf-ja
162 | cd %BUILDDIR%/..
163 | echo.
164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex.
165 | goto end
166 | )
167 |
168 | if "%1" == "text" (
169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
170 | if errorlevel 1 exit /b 1
171 | echo.
172 | echo.Build finished. The text files are in %BUILDDIR%/text.
173 | goto end
174 | )
175 |
176 | if "%1" == "man" (
177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
178 | if errorlevel 1 exit /b 1
179 | echo.
180 | echo.Build finished. The manual pages are in %BUILDDIR%/man.
181 | goto end
182 | )
183 |
184 | if "%1" == "texinfo" (
185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
186 | if errorlevel 1 exit /b 1
187 | echo.
188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
189 | goto end
190 | )
191 |
192 | if "%1" == "gettext" (
193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
194 | if errorlevel 1 exit /b 1
195 | echo.
196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
197 | goto end
198 | )
199 |
200 | if "%1" == "changes" (
201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
202 | if errorlevel 1 exit /b 1
203 | echo.
204 | echo.The overview file is in %BUILDDIR%/changes.
205 | goto end
206 | )
207 |
208 | if "%1" == "linkcheck" (
209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
210 | if errorlevel 1 exit /b 1
211 | echo.
212 | echo.Link check complete; look for any errors in the above output ^
213 | or in %BUILDDIR%/linkcheck/output.txt.
214 | goto end
215 | )
216 |
217 | if "%1" == "doctest" (
218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
219 | if errorlevel 1 exit /b 1
220 | echo.
221 | echo.Testing of doctests in the sources finished, look at the ^
222 | results in %BUILDDIR%/doctest/output.txt.
223 | goto end
224 | )
225 |
226 | if "%1" == "xml" (
227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
228 | if errorlevel 1 exit /b 1
229 | echo.
230 | echo.Build finished. The XML files are in %BUILDDIR%/xml.
231 | goto end
232 | )
233 |
234 | if "%1" == "pseudoxml" (
235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
236 | if errorlevel 1 exit /b 1
237 | echo.
238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
239 | goto end
240 | )
241 |
242 | :end
243 |
--------------------------------------------------------------------------------
/authorize/apis/recurring_api.py:
--------------------------------------------------------------------------------
1 | from authorize.apis.base_api import BaseAPI
2 | from authorize.schemas import CreateRecurringSchema
3 | from authorize.schemas import UpdateRecurringSchema
4 | from authorize.schemas import ListRecurringSchema
5 | from authorize.xml_data import *
6 |
7 |
8 | class RecurringAPI(BaseAPI):
9 |
10 | def create(self, params={}):
11 | subscription = self._deserialize(CreateRecurringSchema().bind(), params)
12 | return self.api._make_call(self._create_request(subscription))
13 |
14 | def details(self, subscription_id):
15 | return self.api._make_call(self._details_request(subscription_id))
16 |
17 | def status(self, subscription_id):
18 | return self.api._make_call(self._status_request(subscription_id))
19 |
20 | def update(self, subscription_id, params={}):
21 | subscription = self._deserialize(UpdateRecurringSchema().bind(), params)
22 | return self.api._make_call(self._update_request(subscription_id, subscription))
23 |
24 | def delete(self, subscription_id):
25 | return self.api._make_call(self._delete_request(subscription_id))
26 |
27 | def list(self, params={}):
28 | """
29 | Required Parameters:
30 | * searchType (str)
31 | - cardExpiringThisMonth
32 | - subscriptionActive
33 | - subscriptionInactive
34 | - subscriptionExpiringThisMonth
35 |
36 | Optional Parameters:
37 | * sorting
38 | * orderBy (string)
39 | - id
40 | - name
41 | - status
42 | - createTimeStampUTC
43 | - lastName
44 | - firstName
45 | - accountNumber (ordered by last 4 digits only)
46 | - amount
47 | - pastOccurrences
48 | * orderDescending (bool)
49 | * paging
50 | * limit (int) (1-1000)
51 | * offset (int) (1-100000)
52 | """
53 | paging = self._deserialize(ListRecurringSchema().bind(), params)
54 | return self.api._make_call(self._list_request(paging))
55 |
56 | # The following methods generate the XML for the corresponding API calls.
57 | # This makes unit testing each of the calls easier.
58 | def _create_request(self, subscription={}):
59 | return self._make_xml('ARBCreateSubscriptionRequest', None, params=subscription)
60 |
61 | def _details_request(self, subscription_id):
62 | request = self.api._base_request('ARBGetSubscriptionRequest')
63 | E.SubElement(request, 'subscriptionId').text = subscription_id
64 | return request
65 |
66 | def _status_request(self, subscription_id):
67 | request = self.api._base_request('ARBGetSubscriptionStatusRequest')
68 | E.SubElement(request, 'subscriptionId').text = subscription_id
69 | return request
70 |
71 | def _update_request(self, subscription_id, subscription={}):
72 | return self._make_xml('ARBUpdateSubscriptionRequest', subscription_id, params=subscription)
73 |
74 | def _delete_request(self, subscription_id):
75 | request = self.api._base_request('ARBCancelSubscriptionRequest')
76 | E.SubElement(request, 'subscriptionId').text = subscription_id
77 | return request
78 |
79 | def _list_request(self, params):
80 | request = self.api._base_request('ARBGetSubscriptionListRequest')
81 |
82 | if 'search_type' in params:
83 | E.SubElement(request, 'searchType').text = params['search_type']
84 |
85 | if 'sorting' in params:
86 | sorting = E.SubElement(request, 'sorting')
87 | E.SubElement(sorting, 'orderBy').text = params['sorting']['order_by']
88 | E.SubElement(sorting, 'orderDescending').text = str(int(params['sorting']['order_descending']))
89 |
90 | if 'paging' in params:
91 | paging = E.SubElement(request, 'paging')
92 | E.SubElement(paging, 'limit').text = str(params['paging']['limit'])
93 | E.SubElement(paging, 'offset').text = str(params['paging']['offset'])
94 |
95 | return request
96 |
97 | def _make_xml(self, method, subscription_id=None, params={}):
98 | request = self.api._base_request(method)
99 |
100 | if subscription_id:
101 | E.SubElement(request, 'subscriptionId').text = subscription_id
102 |
103 | subscription = E.SubElement(request, 'subscription')
104 |
105 | if 'name' in params:
106 | E.SubElement(subscription, 'name').text = params['name']
107 |
108 | # Payment schedule
109 | schedule = E.SubElement(subscription, 'paymentSchedule')
110 | if subscription_id is None:
111 | interval = E.SubElement(schedule, 'interval')
112 | E.SubElement(interval, 'length').text = str(params['interval_length'])
113 | E.SubElement(interval, 'unit').text = params['interval_unit']
114 |
115 | if 'start_date' in params:
116 | E.SubElement(schedule, 'startDate').text = str(params['start_date'])
117 |
118 | if 'total_occurrences' in params:
119 | E.SubElement(schedule, 'totalOccurrences').text = str(params['total_occurrences'])
120 |
121 | if 'trial_occurrences' in params:
122 | E.SubElement(schedule, 'trialOccurrences').text = str(params['trial_occurrences'])
123 |
124 | if 'amount' in params:
125 | E.SubElement(subscription, 'amount').text = quantize(params['amount'])
126 |
127 | if 'trial_amount' in params:
128 | E.SubElement(subscription, 'trialAmount').text = quantize(params['trial_amount'])
129 |
130 | # Payment information
131 | if 'credit_card' in params:
132 | subscription.append(create_payment(params['credit_card']))
133 | if 'bank_account' in params:
134 | subscription.append(create_payment(params['bank_account']))
135 | if 'opaque_data' in params:
136 | subscription.append(create_payment(params['opaque_data']))
137 | if 'profile' in params:
138 | profile = E.SubElement(subscription, 'profile')
139 |
140 | if 'customer_id' in params['profile'].keys():
141 | E.SubElement(profile, 'customerProfileId').text = params['profile']['customer_id']
142 | if 'payment_id' in params['profile'].keys():
143 | E.SubElement(profile, 'customerPaymentProfileId').text = params['profile']['payment_id']
144 | if 'address_id' in params['profile'].keys():
145 | E.SubElement(profile, 'customerAddressId').text = params['profile']['address_id']
146 |
147 | if 'order' in params:
148 | subscription.append(create_order(params['order']))
149 |
150 | if 'customer' in params:
151 | customer = E.SubElement(subscription, 'customer')
152 | E.SubElement(customer, 'id').text = params['customer']['merchant_id']
153 | if 'email' in params['customer']:
154 | E.SubElement(customer, 'email').text = params['customer']['email']
155 |
156 | # A very obscure bug exists that will throw an error if no last name
157 | # or first name is provided for billing.
158 | # Issue 26: Don't set these billing fields if there is already a
159 | # subscription.
160 | if subscription_id is None and not 'profile' in params:
161 | arb_required_fields = {
162 | 'billing': {
163 | 'first_name': '',
164 | 'last_name': ''
165 | }
166 | }
167 | arb_required_fields.update(params)
168 | params = arb_required_fields
169 |
170 | if 'billing' in params:
171 | subscription.append(create_address('billTo', params['billing']))
172 |
173 | if 'shipping' in params:
174 | subscription.append(create_address('shipTo', params['shipping']))
175 |
176 | return request
177 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = _build
9 | GH_PAGES_SOURCE = docs Makefile
10 |
11 | # User-friendly check for sphinx-build
12 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
13 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
14 | endif
15 |
16 | # Internal variables.
17 | PAPEROPT_a4 = -D latex_paper_size=a4
18 | PAPEROPT_letter = -D latex_paper_size=letter
19 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) docs
20 | # the i18n builder cannot share the environment and doctrees with the others
21 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) docs
22 |
23 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
24 |
25 | help:
26 | @echo "Please use \`make ' where is one of"
27 | @echo " html to make standalone HTML files"
28 | @echo " dirhtml to make HTML files named index.html in directories"
29 | @echo " singlehtml to make a single large HTML file"
30 | @echo " pickle to make pickle files"
31 | @echo " json to make JSON files"
32 | @echo " htmlhelp to make HTML files and a HTML help project"
33 | @echo " qthelp to make HTML files and a qthelp project"
34 | @echo " devhelp to make HTML files and a Devhelp project"
35 | @echo " epub to make an epub"
36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
37 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
39 | @echo " text to make text files"
40 | @echo " man to make manual pages"
41 | @echo " texinfo to make Texinfo files"
42 | @echo " info to make Texinfo files and run them through makeinfo"
43 | @echo " gettext to make PO message catalogs"
44 | @echo " changes to make an overview of all changed/added/deprecated items"
45 | @echo " xml to make Docutils-native XML files"
46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
47 | @echo " linkcheck to check all external links for integrity"
48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
49 |
50 | clean:
51 | rm -rf $(BUILDDIR)/*
52 |
53 | html:
54 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
55 | @echo
56 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
57 |
58 | gh-pages:
59 | git checkout gh-pages
60 | rm -rf $(GH_PAGES_SOURCE) $(BUILDDIR) _sources _static
61 | git checkout master $(GH_PAGES_SOURCE)
62 | git reset HEAD
63 | make html
64 | mv -fv _build/html/* ./
65 | rm -rf $(GH_PAGES_SOURCE) $(BUILDDIR)
66 | git add -A
67 | git commit -m "Generated gh-pages for `git log master -1 --pretty=short --abbrev-commit`" && git push origin gh-pages ; git checkout master
68 |
69 | dirhtml:
70 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
71 | @echo
72 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
73 |
74 | singlehtml:
75 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
76 | @echo
77 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
78 |
79 | pickle:
80 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
81 | @echo
82 | @echo "Build finished; now you can process the pickle files."
83 |
84 | json:
85 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
86 | @echo
87 | @echo "Build finished; now you can process the JSON files."
88 |
89 | htmlhelp:
90 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
91 | @echo
92 | @echo "Build finished; now you can run HTML Help Workshop with the" \
93 | ".hhp project file in $(BUILDDIR)/htmlhelp."
94 |
95 | qthelp:
96 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
97 | @echo
98 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
99 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
100 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Py-Authorize.qhcp"
101 | @echo "To view the help file:"
102 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Py-Authorize.qhc"
103 |
104 | devhelp:
105 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
106 | @echo
107 | @echo "Build finished."
108 | @echo "To view the help file:"
109 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Py-Authorize"
110 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Py-Authorize"
111 | @echo "# devhelp"
112 |
113 | epub:
114 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
115 | @echo
116 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
117 |
118 | latex:
119 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
120 | @echo
121 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
122 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
123 | "(use \`make latexpdf' here to do that automatically)."
124 |
125 | latexpdf:
126 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
127 | @echo "Running LaTeX files through pdflatex..."
128 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
129 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
130 |
131 | latexpdfja:
132 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
133 | @echo "Running LaTeX files through platex and dvipdfmx..."
134 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
135 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
136 |
137 | text:
138 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
139 | @echo
140 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
141 |
142 | man:
143 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
144 | @echo
145 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
146 |
147 | texinfo:
148 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
149 | @echo
150 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
151 | @echo "Run \`make' in that directory to run these through makeinfo" \
152 | "(use \`make info' here to do that automatically)."
153 |
154 | info:
155 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
156 | @echo "Running Texinfo files through makeinfo..."
157 | make -C $(BUILDDIR)/texinfo info
158 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
159 |
160 | gettext:
161 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
162 | @echo
163 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
164 |
165 | changes:
166 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
167 | @echo
168 | @echo "The overview file is in $(BUILDDIR)/changes."
169 |
170 | linkcheck:
171 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
172 | @echo
173 | @echo "Link check complete; look for any errors in the above output " \
174 | "or in $(BUILDDIR)/linkcheck/output.txt."
175 |
176 | doctest:
177 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
178 | @echo "Testing of doctests in the sources finished, look at the " \
179 | "results in $(BUILDDIR)/doctest/output.txt."
180 |
181 | xml:
182 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
183 | @echo
184 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
185 |
186 | pseudoxml:
187 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
188 | @echo
189 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
190 |
--------------------------------------------------------------------------------
/authorize/xml_data.py:
--------------------------------------------------------------------------------
1 | import xml.etree.cElementTree as E
2 |
3 | from decimal import Decimal
4 | from xml.dom import minidom
5 |
6 |
7 | def create_profile(params={}):
8 | profile = E.Element('profile')
9 | E.SubElement(profile, 'merchantCustomerId').text = params['merchant_id']
10 | if 'description' in params:
11 | E.SubElement(profile, 'description').text = params['description']
12 | if 'email' in params:
13 | E.SubElement(profile, 'email').text = params['email']
14 | return profile
15 |
16 |
17 | def create_customer(params={}):
18 | customer = E.Element('customer')
19 | E.SubElement(customer, 'merchantCustomerId').text = params['merchant_id']
20 | if 'description' in params:
21 | E.SubElement(customer, 'description').text = params['description']
22 | if 'email' in params:
23 | E.SubElement(customer, 'email').text = params['email']
24 | return customer
25 |
26 |
27 | def create_address(name, params={}):
28 | address = E.Element(name)
29 | if 'first_name' in params:
30 | E.SubElement(address, 'firstName').text = params['first_name']
31 | if 'last_name' in params:
32 | E.SubElement(address, 'lastName').text = params['last_name']
33 | if 'company' in params:
34 | E.SubElement(address, 'company').text = params['company']
35 | if 'address' in params:
36 | E.SubElement(address, 'address').text = params['address']
37 | if 'city' in params:
38 | E.SubElement(address, 'city').text = params['city']
39 | if 'state' in params:
40 | E.SubElement(address, 'state').text = params['state']
41 | if 'zip' in params:
42 | E.SubElement(address, 'zip').text = params['zip']
43 | if 'country' in params:
44 | E.SubElement(address, 'country').text = params['country']
45 | if 'phone_number' in params:
46 | E.SubElement(address, 'phoneNumber').text = params['phone_number']
47 | if 'fax_number' in params:
48 | E.SubElement(address, 'faxNumber').text = params['fax_number']
49 | return address
50 |
51 |
52 | def create_card(params={}):
53 | card = E.Element('creditCard')
54 | date = params['expiration_year'] + '-' + params['expiration_month']
55 | E.SubElement(card, 'cardNumber').text = params['card_number']
56 | E.SubElement(card, 'expirationDate').text = date
57 | if 'card_code' in params:
58 | E.SubElement(card, 'cardCode').text = str(params['card_code'])
59 | return card
60 |
61 |
62 | def create_opaque_data(params={}):
63 | data = E.Element('opaqueData')
64 | E.SubElement(data, 'dataDescriptor').text = params['data_descriptor']
65 | E.SubElement(data, 'dataValue').text = params['data_value']
66 | if 'data_key' in params:
67 | E.SubElement(data, 'dataKey').text = str(params['data_key'])
68 | return data
69 |
70 |
71 | def format_tracks(params={}):
72 | tracks = E.Element('trackData')
73 | if 'track_1' in params:
74 | E.SubElement(tracks, 'track1').text = params['track_1']
75 | elif 'track_2' in params:
76 | E.SubElement(tracks, 'track2').text = params['track_2']
77 | return tracks
78 |
79 |
80 | def set_retail(params={}):
81 | retail = E.Element('retail')
82 | E.SubElement(retail, 'marketType').text = str(params['market_type'])
83 | E.SubElement(retail, 'deviceType').text = str(params['device_type'])
84 | return retail
85 |
86 |
87 | def create_account(params={}):
88 | account = E.Element('bankAccount')
89 | if 'account_type' in params:
90 | E.SubElement(account, 'accountType').text = params['account_type']
91 | E.SubElement(account, 'routingNumber').text = params['routing_number']
92 | E.SubElement(account, 'accountNumber').text = params['account_number']
93 | E.SubElement(account, 'nameOnAccount').text = params['name_on_account']
94 | if 'echeck_type' in params:
95 | E.SubElement(account, 'echeckType').text = params['echeck_type']
96 | if 'bank_name' in params:
97 | E.SubElement(account, 'bankName').text = params['bank_name']
98 | return account
99 |
100 |
101 | def create_pay_pal(params={}):
102 | pay_pal = E.Element('payPal')
103 | if 'success_url' in params:
104 | E.SubElement(pay_pal, 'successUrl').text = params['success_url']
105 | if 'cancel_url' in params:
106 | E.SubElement(pay_pal, 'cancelUrl').text = params['cancel_url']
107 | if 'locale' in params:
108 | E.SubElement(pay_pal, 'paypalLc').text = params['locale']
109 | if 'header_image' in params:
110 | E.SubElement(pay_pal, 'paypalHdrImg').text = params['header_image']
111 | if 'flow_color' in params:
112 | E.SubElement(pay_pal, 'paypalPayflowcolor').text = params['flow_color']
113 | return pay_pal
114 |
115 |
116 | def create_line_item(name, params={}):
117 | item = E.Element(name)
118 | if 'item_id' in params:
119 | E.SubElement(item, 'itemId').text = params['item_id']
120 | if 'name' in params:
121 | E.SubElement(item, 'name').text = params['name']
122 | if 'description' in params:
123 | E.SubElement(item, 'description').text = params['description']
124 | if 'quantity' in params:
125 | E.SubElement(item, 'quantity').text = str(params['quantity'])
126 | if 'unit_price' in params:
127 | E.SubElement(item, 'unitPrice').text = quantize(params['unit_price'])
128 | if 'taxable' in params:
129 | E.SubElement(item, 'taxable').text = str(params['taxable']).lower()
130 | return item
131 |
132 |
133 | def create_line_items(items=[]):
134 | line_items = E.Element('lineItems')
135 | for item in items:
136 | line_items.append(create_line_item('lineItem', item))
137 | return line_items
138 |
139 |
140 | def create_user_fields(fields=[]):
141 | user_fields = E.Element('userFields')
142 | for field in fields:
143 | item = E.Element('userField')
144 | E.SubElement(item, 'name').text = field['name']
145 | E.SubElement(item, 'value').text = field['value']
146 | user_fields.append(item)
147 | return user_fields
148 |
149 |
150 | def create_amount_type(name, params={}):
151 | amount_type = E.Element(name)
152 | if 'amount' in params:
153 | E.SubElement(amount_type, 'amount').text = quantize(params['amount'])
154 | if 'name' in params:
155 | E.SubElement(amount_type, 'name').text = params['name']
156 | if 'description' in params:
157 | E.SubElement(amount_type, 'description').text = params['description']
158 | return amount_type
159 |
160 |
161 | def create_order(params={}):
162 | order = E.Element('order')
163 | if 'invoice_number' in params:
164 | E.SubElement(order, 'invoiceNumber').text = params['invoice_number']
165 | if 'description' in params:
166 | E.SubElement(order, 'description').text = params['description']
167 | return order
168 |
169 |
170 | def create_payment(params={}):
171 | payment = E.Element('payment')
172 | # If a card_number key exists here, we know that we are dealing with a
173 | # credit card. Otherwise, it's a bank account.
174 | if 'card_number' in params:
175 | payment.append(create_card(params))
176 | elif 'track_data' in params:
177 | payment.append(format_tracks(params))
178 | elif 'data_descriptor' in params:
179 | payment.append(create_opaque_data(params))
180 | else:
181 | payment.append(create_account(params))
182 | return payment
183 |
184 |
185 | def create_transaction_settings(params):
186 | e = E.Element('transactionSettings')
187 | if 'duplicate_window' in params:
188 | e.append(create_transaction_setting('duplicateWindow', str(params['duplicate_window'])))
189 | return e
190 |
191 |
192 | def create_transaction_setting(name, value):
193 | e = E.Element('setting')
194 | E.SubElement(e, 'settingName').text = name
195 | E.SubElement(e, 'settingValue').text = value
196 | return e
197 |
198 |
199 | def quantize(amount):
200 | return str(Decimal(str(amount)).quantize(Decimal('0.01')))
201 |
202 |
203 | def prettify(elem):
204 | """Return a pretty-printed XML string for the Element."""
205 | rough_string = E.tostring(elem, 'utf-8')
206 | reparsed = minidom.parseString(rough_string)
207 | return reparsed.toprettyxml(indent=' ').strip()
208 |
--------------------------------------------------------------------------------
/tests/test_credit_card_api.py:
--------------------------------------------------------------------------------
1 | from authorize.configuration import Configuration
2 | from authorize.xml_data import prettify
3 |
4 | from unittest import TestCase
5 |
6 | CREDIT_CARD = {
7 | 'customer_type': 'individual',
8 | 'card_number': '4111111111111111',
9 | 'card_code': '456',
10 | 'expiration_month': '04',
11 | 'expiration_year': '2014',
12 | 'billing': {
13 | 'first_name': 'Rob',
14 | 'last_name': 'Oteron',
15 | 'company': 'Robotron Studios',
16 | 'address': '101 Computer Street',
17 | 'city': 'Tucson',
18 | 'state': 'AZ',
19 | 'zip': '85704',
20 | 'country': 'US',
21 | 'phone_number': '520-123-4567',
22 | 'fax_number': '520-456-7890',
23 | },
24 | }
25 |
26 | # Update the credit card information except the card number
27 | UPDATE_CREDIT_CARD_INFO = {
28 | 'customer_type': 'individual',
29 | 'card_number': '1111',
30 | 'card_code': '456',
31 | 'expiration_month': '04',
32 | 'expiration_year': '2014',
33 | 'billing': {
34 | 'first_name': 'Rob',
35 | 'last_name': 'Oteron',
36 | 'company': 'Robotron Studios',
37 | 'address': '101 Computer Street',
38 | 'city': 'Tucson',
39 | 'state': 'AZ',
40 | 'zip': '85704',
41 | 'country': 'US',
42 | 'phone_number': '520-123-4567',
43 | 'fax_number': '520-456-7890',
44 | },
45 | }
46 |
47 | VALIDATE_CREDIT_CARD = {
48 | 'address_id': '7982053235',
49 | 'card_code': '456',
50 | 'validation_mode': 'testMode',
51 | }
52 |
53 | CREATE_CREDIT_CARD_REQUEST = '''
54 |
55 |
56 |
57 | 8s8tVnG5t
58 | 5GK7mncw8mG2946z
59 |
60 | 1234567890
61 |
62 | individual
63 |
64 | Rob
65 | Oteron
66 | Robotron Studios
67 | 101 Computer Street
68 | Tucson
69 | AZ
70 | 85704
71 | US
72 | 520-123-4567
73 | 520-456-7890
74 |
75 |
76 |
77 | 4111111111111111
78 | 2014-04
79 | 456
80 |
81 |
82 |
83 |
84 | '''
85 |
86 | DETAILS_CREDIT_CARD_REQUEST = '''
87 |
88 |
89 |
90 | 8s8tVnG5t
91 | 5GK7mncw8mG2946z
92 |
93 | 1234567890
94 | 0987654321
95 | true
96 |
97 | '''
98 |
99 | UPDATE_CREDIT_CARD_REQUEST = '''
100 |
101 |
102 |
103 | 8s8tVnG5t
104 | 5GK7mncw8mG2946z
105 |
106 | 1234567890
107 |
108 | individual
109 |
110 | Rob
111 | Oteron
112 | Robotron Studios
113 | 101 Computer Street
114 | Tucson
115 | AZ
116 | 85704
117 | US
118 | 520-123-4567
119 | 520-456-7890
120 |
121 |
122 |
123 | 4111111111111111
124 | 2014-04
125 | 456
126 |
127 |
128 | 0987654321
129 |
130 |
131 | '''
132 |
133 | UPDATE_CREDIT_CARD_INFO_REQUEST = '''
134 |
135 |
136 |
137 | 8s8tVnG5t
138 | 5GK7mncw8mG2946z
139 |
140 | 1234567890
141 |
142 | individual
143 |
144 | Rob
145 | Oteron
146 | Robotron Studios
147 | 101 Computer Street
148 | Tucson
149 | AZ
150 | 85704
151 | US
152 | 520-123-4567
153 | 520-456-7890
154 |
155 |
156 |
157 | XXXX1111
158 | 2014-04
159 | 456
160 |
161 |
162 | 0987654321
163 |
164 |
165 | '''
166 |
167 | DELETE_CREDIT_CARD_REQUEST = '''
168 |
169 |
170 |
171 | 8s8tVnG5t
172 | 5GK7mncw8mG2946z
173 |
174 | 1234567890
175 | 0987654321
176 |
177 | '''
178 |
179 | VALIDATE_CREDIT_CARD_REQUEST = '''
180 |
181 |
182 |
183 | 8s8tVnG5t
184 | 5GK7mncw8mG2946z
185 |
186 | 1234567890
187 | 0987654321
188 | 7982053235
189 | 456
190 | testMode
191 |
192 | '''
193 |
194 |
195 | class CreditCardAPITests(TestCase):
196 |
197 | maxDiff = None
198 |
199 | def test_create_credit_card_request(self):
200 | request_xml = Configuration.api.credit_card._create_request('1234567890', CREDIT_CARD)
201 | request_string = prettify(request_xml)
202 | self.assertEqual(request_string, CREATE_CREDIT_CARD_REQUEST.strip())
203 |
204 | def test_details_credit_card_request(self):
205 | request_xml = Configuration.api.credit_card._details_request('1234567890', '0987654321')
206 | request_string = prettify(request_xml)
207 | self.assertEqual(request_string, DETAILS_CREDIT_CARD_REQUEST.strip())
208 |
209 | def test_update_credit_card_request(self):
210 | request_xml = Configuration.api.credit_card._update_request('1234567890', '0987654321', CREDIT_CARD)
211 | request_string = prettify(request_xml)
212 | self.assertEqual(request_string, UPDATE_CREDIT_CARD_REQUEST.strip())
213 |
214 | def test_update_credit_card_info_request(self):
215 | request_xml = Configuration.api.credit_card._update_request('1234567890', '0987654321', UPDATE_CREDIT_CARD_INFO)
216 | request_string = prettify(request_xml)
217 | self.assertEqual(request_string, UPDATE_CREDIT_CARD_INFO_REQUEST.strip())
218 |
219 | def test_delete_credit_card_request(self):
220 | request_xml = Configuration.api.credit_card._delete_request('1234567890', '0987654321')
221 | request_string = prettify(request_xml)
222 | self.assertEqual(request_string, DELETE_CREDIT_CARD_REQUEST.strip())
223 |
224 | def test_validate_credit_card_request(self):
225 | request_xml = Configuration.api.credit_card._validate_request('1234567890', '0987654321', VALIDATE_CREDIT_CARD)
226 | request_string = prettify(request_xml)
227 | self.assertEqual(request_string, VALIDATE_CREDIT_CARD_REQUEST.strip())
228 |
--------------------------------------------------------------------------------
/authorize/apis/transaction_api.py:
--------------------------------------------------------------------------------
1 | try:
2 | import urllib.parse as urllib
3 | except:
4 | import urllib
5 |
6 | from authorize.apis.base_api import BaseAPI
7 | from authorize.schemas import AIMTransactionSchema
8 | from authorize.schemas import CreditTransactionSchema
9 | from authorize.schemas import RefundTransactionSchema
10 | from authorize.xml_data import *
11 |
12 |
13 | class TransactionAPI(BaseAPI):
14 |
15 | def sale(self, params={}):
16 | xact = self._deserialize(AIMTransactionSchema(), params)
17 | return self.api._make_call(self._transaction_request('authCaptureTransaction', xact))
18 |
19 | def auth(self, params={}):
20 | xact = self._deserialize(AIMTransactionSchema(), params)
21 | return self.api._make_call(self._transaction_request('authOnlyTransaction', xact))
22 |
23 | def settle(self, transaction_id, amount=None):
24 | return self.api._make_call(self._settle_request(transaction_id, amount))
25 |
26 | def credit(self, params={}):
27 | xact = self._deserialize(CreditTransactionSchema(), params)
28 | return self.api._make_call(self._transaction_request('refundTransaction', xact))
29 |
30 | def auth_continue(self, transaction_id, payer_id):
31 | return self.api._make_call(self._pay_pal_continue_request('authOnlyContinueTransaction', transaction_id,
32 | payer_id))
33 |
34 | def sale_continue(self, transaction_id, payer_id):
35 | return self.api._make_call(self._pay_pal_continue_request('authCaptureContinueTransaction', transaction_id,
36 | payer_id))
37 |
38 | def refund(self, params={}):
39 | xact = self._deserialize(RefundTransactionSchema(), params)
40 | return self.api._make_call(self._refund_request(xact))
41 |
42 | def void(self, transaction_id):
43 | return self.api._make_call(self._void_request(transaction_id))
44 |
45 | def details(self, transaction_id):
46 | return self.api._make_call(self._details_request(transaction_id))
47 |
48 | def list(self, batch_id):
49 | if batch_id:
50 | return self.api._make_call(self._settled_list_request(batch_id))
51 | else:
52 | return self.api._make_call(self._unsettled_list_request())
53 |
54 | def _transaction_request(self, xact_type, xact={}):
55 | is_cim = 'customer_id' in xact
56 |
57 | request = self.api._base_request('createTransactionRequest')
58 |
59 | xact_elem = E.SubElement(request, 'transactionRequest')
60 | E.SubElement(xact_elem, 'transactionType').text = xact_type
61 | E.SubElement(xact_elem, 'amount').text = quantize(xact['amount'])
62 |
63 | if 'currency_code' in xact:
64 | E.SubElement(xact_elem, 'currencyCode').text = xact['currency_code']
65 |
66 | # CIM information
67 | if is_cim: # customerProfilePaymentType
68 | profile = E.SubElement(xact_elem, 'profile')
69 |
70 | # TODO - determine how to create a new customer here...
71 |
72 | E.SubElement(profile, 'customerProfileId').text = xact['customer_id']
73 |
74 | payment = E.SubElement(profile, 'paymentProfile')
75 | E.SubElement(payment, 'paymentProfileId').text = xact['payment_id']
76 |
77 | if 'address_id' in xact:
78 | E.SubElement(profile, 'shippingProfileId').text = xact['address_id']
79 | else:
80 | payment = E.SubElement(xact_elem, 'payment')
81 | if 'credit_card' in xact:
82 | payment.append(create_card(xact['credit_card']))
83 | elif 'opaque_data' in xact:
84 | payment.append(create_opaque_data(xact['opaque_data']))
85 | elif 'track_data' in xact:
86 | payment.append(format_tracks(xact['track_data']))
87 | elif 'bank_account' in xact:
88 | payment.append(create_account(xact['bank_account']))
89 | elif 'pay_pal' in xact:
90 | payment.append(create_pay_pal(xact['pay_pal']))
91 |
92 | if 'order' in xact:
93 | xact_elem.append(create_order(xact['order']))
94 |
95 | if 'line_items' in xact:
96 | xact_elem.append(create_line_items(xact['line_items']))
97 |
98 | if 'tax' in xact:
99 | xact_elem.append(create_amount_type('tax', xact['tax']))
100 |
101 | if 'duty' in xact:
102 | xact_elem.append(create_amount_type('duty', xact['duty']))
103 |
104 | if 'shipping_and_handling' in xact:
105 | xact_elem.append(create_amount_type('shipping', xact['shipping_and_handling']))
106 |
107 | if 'tax_exempt' in xact:
108 | E.SubElement(xact_elem, 'taxExempt').text = str(xact['tax_exempt']).lower()
109 |
110 | if 'po_number' in xact:
111 | E.SubElement(xact_elem, 'poNumber').text = xact['po_number']
112 |
113 | if 'email' in xact:
114 | customer = E.SubElement(xact_elem, 'customer')
115 | E.SubElement(customer, 'email').text = xact['email']
116 |
117 | if not is_cim and 'billing' in xact:
118 | xact_elem.append(create_address('billTo', xact['billing']))
119 |
120 | if not is_cim and 'shipping' in xact:
121 | xact_elem.append(create_address('shipTo', xact['shipping']))
122 |
123 | if 'customer_ip' in xact:
124 | E.SubElement(xact_elem, 'customerIP').text = xact['customer_ip']
125 |
126 | if 'retail' in xact:
127 | xact_elem.append(set_retail(xact['retail']))
128 |
129 | if 'transaction_settings' in xact:
130 | xact_elem.append(create_transaction_settings(xact['transaction_settings']))
131 |
132 | if 'user_fields' in xact:
133 | xact_elem.append(create_user_fields(xact['user_fields']))
134 |
135 | return request
136 |
137 | def _settle_request(self, transaction_id, amount):
138 | request = self.api._base_request('createTransactionRequest')
139 | xact_elem = E.SubElement(request, 'transactionRequest')
140 | E.SubElement(xact_elem, 'transactionType').text = 'priorAuthCaptureTransaction'
141 |
142 | if amount:
143 | E.SubElement(xact_elem, 'amount').text = quantize(amount)
144 |
145 | E.SubElement(xact_elem, 'refTransId').text = transaction_id
146 | return request
147 |
148 | def _pay_pal_continue_request(self, xact_type, transaction_id, payer_id):
149 | request = self.api._base_request('createTransactionRequest')
150 | xact_elem = E.SubElement(request, 'transactionRequest')
151 | E.SubElement(xact_elem, 'transactionType').text = xact_type
152 |
153 | payment = E.Element('payment')
154 | pay_pal = E.SubElement(payment, 'payPal')
155 | E.SubElement(pay_pal, 'payerID').text = payer_id
156 | xact_elem.append(payment)
157 |
158 | E.SubElement(xact_elem, 'refTransId').text = transaction_id
159 | return request
160 |
161 | def _refund_request(self, xact):
162 | request = self.api._base_request('createTransactionRequest')
163 | xact_elem = E.SubElement(request, 'transactionRequest')
164 | E.SubElement(xact_elem, 'transactionType').text = 'refundTransaction'
165 | E.SubElement(xact_elem, 'amount').text = quantize(xact['amount'])
166 | payment = E.SubElement(xact_elem, 'payment')
167 | credit_card = E.SubElement(payment, 'creditCard')
168 | E.SubElement(credit_card, 'cardNumber').text = xact['last_four'][-4:]
169 | # Authorize.net doesn't care about the actual date
170 | E.SubElement(credit_card, 'expirationDate').text = 'XXXXXX'
171 | E.SubElement(xact_elem, 'refTransId').text = xact['transaction_id']
172 | if 'order' in xact:
173 | xact_elem.append(create_order(xact['order']))
174 | return request
175 |
176 | def _void_request(self, transaction_id):
177 | request = self.api._base_request('createTransactionRequest')
178 | xact_elem = E.SubElement(request, 'transactionRequest')
179 | E.SubElement(xact_elem, 'transactionType').text = 'voidTransaction'
180 | E.SubElement(xact_elem, 'refTransId').text = transaction_id
181 | return request
182 |
183 | def _details_request(self, transaction_id):
184 | request = self.api._base_request('getTransactionDetailsRequest')
185 | E.SubElement(request, 'transId').text = transaction_id
186 | return request
187 |
188 | def _unsettled_list_request(self):
189 | request = self.api._base_request('getUnsettledTransactionListRequest')
190 | return request
191 |
192 | def _settled_list_request(self, batch_id):
193 | request = self.api._base_request('getTransactionListRequest')
194 | E.SubElement(request, 'batchId').text = batch_id
195 | return request
196 |
--------------------------------------------------------------------------------
/tests/test_response_parser.py:
--------------------------------------------------------------------------------
1 | import xml.etree.cElementTree as E
2 |
3 | from unittest import TestCase
4 |
5 | from authorize.response_parser import parse_response
6 |
7 |
8 | SINGLE_LIST_ITEM_RESPONSE_XML = '''
9 |
10 |
11 |
12 | Ok
13 |
14 | I00001
15 | Successful.
16 |
17 |
18 |
19 | '''
20 |
21 | MULTIPLE_LIST_ITEM_RESPONSE_XML = '''
22 |
23 |
24 |
25 | Ok
26 |
27 | I00001
28 | Successful.
29 |
30 |
31 |
32 | Ok
33 |
34 | I00001
35 | Successful.
36 |
37 |
38 |
39 | '''
40 |
41 | NUMERIC_STRING_LIST_RESPONSE_XML = '''
42 |
43 |
44 |
45 | Ok
46 |
47 | I00001
48 | Successful.
49 |
50 |
51 | 24527322
52 |
53 | 22467955
54 | 22467956
55 | 22467957
56 |
57 |
58 |
59 |
60 | '''
61 |
62 | DIRECT_RESPONSE_XML = '''
63 |
64 |
65 |
66 | Ok
67 |
68 | I00001
69 | Successful.
70 |
71 |
72 | 1,1,1,This transaction has been approved.,RUQ2CH,Y,2208147721,INV0001,Just another invoice...,695.31,CC,auth_capture,a9f25ea698324879955d,,,,,,,,,,,,,,,,,,,,45.00,90.00,10.00,FALSE,,DDE9931C84D8D4062EC36DC5E21C22AA,,2,,,,,,,,,,,XXXX1111,Visa,,,,,,,,,,,,,,,,
73 |
74 | '''
75 |
76 | TRANSACTION_LIST_RESPONSE_XML = '''
77 |
78 |
79 |
80 | Ok
81 |
82 | I00001
83 | Successful.
84 |
85 |
86 |
87 |
88 | 2213859708
89 | 2014-05-25T08:40:21Z
90 | 2014-05-25T01:40:21
91 | settledSuccessfully
92 | Robot
93 | Ron
94 | Visa
95 | XXXX1111
96 | 231.00
97 | eCommerce
98 | Card Not Present
99 |
100 | 1652905
101 | 32
102 |
103 |
104 |
105 | 2213858843
106 | 2014-05-25T08:39:15Z
107 | 2014-05-25T01:39:15
108 | settledSuccessfully
109 | Robot
110 | Ron
111 | Visa
112 | XXXX1111
113 | 4022.00
114 | eCommerce
115 | Card Not Present
116 |
117 | 1591888
118 | 37
119 |
120 |
121 |
122 |
123 | '''
124 |
125 | SINGLE_LIST_ITEM_RESPONSE = {
126 | 'messages': [{
127 | 'result_code': 'Ok',
128 | 'message': {
129 | 'text': 'Successful.',
130 | 'code': 'I00001',
131 | },
132 | }]
133 | }
134 |
135 | MULTIPLE_LIST_ITEM_RESPONSE = {
136 | 'messages': [{
137 | 'result_code': 'Ok',
138 | 'message': {
139 | 'text': 'Successful.',
140 | 'code': 'I00001',
141 | },
142 | }, {
143 | 'result_code': 'Ok',
144 | 'message': {
145 | 'text': 'Successful.',
146 | 'code': 'I00001',
147 | },
148 | }]
149 | }
150 |
151 | NUMERIC_STRING_LIST_RESPONSE = [
152 | '22467955',
153 | '22467956',
154 | '22467957',
155 | ]
156 |
157 | TRANSACTION_RESPONSE = {
158 | 'messages': [{
159 | 'result_code': 'Ok',
160 | 'message': {
161 | 'text': 'Successful.',
162 | 'code': 'I00001',
163 | },
164 | }],
165 | 'transaction_response': {
166 | 'cvv_result_code': '',
167 | 'authorization_code': 'RUQ2CH',
168 | 'response_code': '1',
169 | 'amount': '695.31',
170 | 'transaction_type': 'auth_capture',
171 | 'avs_response': 'Y',
172 | 'response_reason_code': '1',
173 | 'response_reason_text': 'This transaction has been approved.',
174 | 'trans_id': '2208147721',
175 | }
176 | }
177 |
178 | TRANSACTION_LIST_RESPONSE = {
179 | 'messages': [{
180 | 'message': {
181 | 'text': 'Successful.',
182 | 'code': 'I00001'
183 | },
184 | 'result_code': 'Ok'
185 | }],
186 | 'transactions': [{
187 | 'first_name': 'Robot',
188 | 'last_name': 'Ron',
189 | 'account_type': 'Visa',
190 | 'submit_time_local': '2014-05-25T01:40:21',
191 | 'product': 'Card Not Present',
192 | 'submit_time_utc': '2014-05-25T08:40:21Z',
193 | 'account_number': 'XXXX1111',
194 | 'market_type': 'eCommerce',
195 | 'transaction_status': 'settledSuccessfully',
196 | 'settle_amount': '231.00',
197 | 'trans_id': '2213859708',
198 | 'subscription': {
199 | 'pay_num': '32',
200 | 'id': '1652905'
201 | }
202 | }, {
203 | 'first_name': 'Robot',
204 | 'last_name': 'Ron',
205 | 'account_type': 'Visa',
206 | 'submit_time_local': '2014-05-25T01:39:15',
207 | 'product': 'Card Not Present',
208 | 'submit_time_utc': '2014-05-25T08:39:15Z',
209 | 'account_number': 'XXXX1111',
210 | 'market_type': 'eCommerce',
211 | 'transaction_status': 'settledSuccessfully',
212 | 'settle_amount': '4022.00',
213 | 'trans_id': '2213858843',
214 | 'subscription': {
215 | 'pay_num': '37',
216 | 'id': '1591888'
217 | }
218 | }]
219 | }
220 |
221 |
222 | class ResponseParserTests(TestCase):
223 |
224 | maxDiff = None
225 |
226 | def test_parse_single_line_item_response(self):
227 | response_element = E.fromstring(SINGLE_LIST_ITEM_RESPONSE_XML.strip())
228 | response = parse_response(response_element)
229 | self.assertEquals(SINGLE_LIST_ITEM_RESPONSE, response)
230 |
231 | def test_parse_multiple_line_item_response(self):
232 | response_element = E.fromstring(MULTIPLE_LIST_ITEM_RESPONSE_XML.strip())
233 | response = parse_response(response_element)
234 | self.assertEquals(MULTIPLE_LIST_ITEM_RESPONSE, response)
235 |
236 | def test_parse_numeric_string_response(self):
237 | response_element = E.fromstring(NUMERIC_STRING_LIST_RESPONSE_XML.strip())
238 | response = parse_response(response_element)
239 | self.assertEquals(NUMERIC_STRING_LIST_RESPONSE, response['payment_ids'])
240 |
241 | def test_parse_direct_resonse(self):
242 | response_element = E.fromstring(DIRECT_RESPONSE_XML.strip())
243 | response = parse_response(response_element)
244 | self.assertEquals(TRANSACTION_RESPONSE, response)
245 |
246 | def test_parse_transaction_list(self):
247 | response_element = E.fromstring(TRANSACTION_LIST_RESPONSE_XML.strip())
248 | response = parse_response(response_element)
249 | self.assertEquals(TRANSACTION_LIST_RESPONSE, response)
250 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Py-Authorize documentation build configuration file, created by
4 | # sphinx-quickstart on Sat Jun 1 18:14:36 2013.
5 | #
6 | # This file is execfile()d with the current directory set to its containing dir.
7 | #
8 | # Note that not all possible configuration values are present in this
9 | # autogenerated file.
10 | #
11 | # All configuration values have a default; values that are commented out
12 | # serve to show the default.
13 |
14 | import sys
15 | import os
16 | import sphinx_rtd_theme
17 |
18 | # If extensions (or modules to document with autodoc) are in another directory,
19 | # add these directories to sys.path here. If the directory is relative to the
20 | # documentation root, use os.path.abspath to make it absolute, like shown here.
21 | #sys.path.insert(0, os.path.abspath('.'))
22 |
23 | # -- General configuration -----------------------------------------------------
24 |
25 | # If your documentation needs a minimal Sphinx version, state it here.
26 | #needs_sphinx = '1.0'
27 |
28 | # Add any Sphinx extension module names here, as strings. They can be extensions
29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
30 | extensions = ['sphinx.ext.autodoc']
31 |
32 | # Add any paths that contain templates here, relative to this directory.
33 | templates_path = ['_templates']
34 |
35 | # The suffix of source filenames.
36 | source_suffix = '.rst'
37 |
38 | # The encoding of source files.
39 | #source_encoding = 'utf-8-sig'
40 |
41 | # The master toctree document.
42 | master_doc = 'index'
43 |
44 | # General information about the project.
45 | project = u'Py-Authorize'
46 | copyright = u'2016, Vincent Catalano'
47 |
48 | # The version info for the project you're documenting, acts as replacement for
49 | # |version| and |release|, also used in various other places throughout the
50 | # built documents.
51 | #
52 | # The short X.Y version.
53 | version = '1.3'
54 | # The full version, including alpha/beta/rc tags.
55 | release = '1.3.0.0'
56 |
57 | # The language for content autogenerated by Sphinx. Refer to documentation
58 | # for a list of supported languages.
59 | #language = None
60 |
61 | # There are two options for replacing |today|: either, you set today to some
62 | # non-false value, then it is used:
63 | #today = ''
64 | # Else, today_fmt is used as the format for a strftime call.
65 | #today_fmt = '%B %d, %Y'
66 |
67 | # List of patterns, relative to source directory, that match files and
68 | # directories to ignore when looking for source files.
69 | exclude_patterns = ['_build']
70 |
71 | # The reST default role (used for this markup: `text`) to use for all documents.
72 | #default_role = None
73 |
74 | # If true, '()' will be appended to :func: etc. cross-reference text.
75 | #add_function_parentheses = True
76 |
77 | # If true, the current module name will be prepended to all description
78 | # unit titles (such as .. function::).
79 | #add_module_names = True
80 |
81 | # If true, sectionauthor and moduleauthor directives will be shown in the
82 | # output. They are ignored by default.
83 | #show_authors = False
84 |
85 | # The name of the Pygments (syntax highlighting) style to use.
86 | pygments_style = 'sphinx'
87 |
88 | # A list of ignored prefixes for module index sorting.
89 | #modindex_common_prefix = []
90 |
91 | # If true, keep warnings as "system message" paragraphs in the built documents.
92 | #keep_warnings = False
93 |
94 |
95 | # -- Options for HTML output ---------------------------------------------------
96 |
97 | # The theme to use for HTML and HTML Help pages. See the documentation for
98 | # a list of builtin themes.
99 | html_theme = 'sphinx_rtd_theme'
100 |
101 | html_context = {
102 | 'description' : 'Py-Authorize is a full-featured Python API for the Authorize.net payment gateway.',
103 | 'base_url' : 'http://vcatalano.github.io/py-authorize'
104 | }
105 |
106 | # Add any paths that contain custom themes here, relative to this directory.
107 | html_theme_path = sphinx_rtd_theme.get_html_theme_path()
108 |
109 | # The name for this set of Sphinx documents. If None, it defaults to
110 | # " v documentation".
111 | #html_title = None
112 |
113 | # A shorter title for the navigation bar. Default is the same as html_title.
114 | #html_short_title = None
115 |
116 | # The name of an image file (relative to this directory) to place at the top
117 | # of the sidebar.
118 | #html_logo = None
119 |
120 | # The name of an image file (within the static path) to use as favicon of the
121 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
122 | # pixels large.
123 | #html_favicon = None
124 |
125 | # Add any paths that contain custom static files (such as style sheets) here,
126 | # relative to this directory. They are copied after the builtin static files,
127 | # so a file named "default.css" will overwrite the builtin "default.css".
128 | html_static_path = ['_static']
129 |
130 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
131 | # using the given strftime format.
132 | #html_last_updated_fmt = '%b %d, %Y'
133 |
134 | # If true, SmartyPants will be used to convert quotes and dashes to
135 | # typographically correct entities.
136 | #html_use_smartypants = True
137 |
138 | # Custom sidebar templates, maps document names to template names.
139 | html_sidebars = {'**': ['globaltoc.html', 'searchbox.html']}
140 |
141 | # Additional templates that should be rendered to pages, maps page names to
142 | # template names.
143 | #html_additional_pages = {}
144 |
145 | # If false, no module index is generated.
146 | #html_domain_indices = True
147 |
148 | # If false, no index is generated.
149 | #html_use_index = True
150 |
151 | # If true, the index is split into individual pages for each letter.
152 | #html_split_index = False
153 |
154 | # If true, links to the reST sources are added to the pages.
155 | #html_show_sourcelink = True
156 |
157 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
158 | #html_show_sphinx = True
159 |
160 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
161 | #html_show_copyright = True
162 |
163 | # If true, an OpenSearch description file will be output, and all pages will
164 | # contain a tag referring to it. The value of this option must be the
165 | # base URL from which the finished HTML is served.
166 | #html_use_opensearch = ''
167 |
168 | # This is the file name suffix for HTML files (e.g. ".xhtml").
169 | #html_file_suffix = None
170 |
171 | # Output file base name for HTML help builder.
172 | htmlhelp_basename = 'Py-AuthorizeDoc'
173 |
174 |
175 | # -- Options for LaTeX output --------------------------------------------------
176 |
177 | latex_elements = {
178 | # The paper size ('letterpaper' or 'a4paper').
179 | #'papersize': 'letterpaper',
180 |
181 | # The font size ('10pt', '11pt' or '12pt').
182 | #'pointsize': '10pt',
183 |
184 | # Additional stuff for the LaTeX preamble.
185 | #'preamble': '',
186 | }
187 |
188 | # Grouping the document tree into LaTeX files. List of tuples
189 | # (source start file, target name, title, author, documentclass [howto/manual]).
190 | latex_documents = [
191 | ('index', 'Py-Authorize.tex', u'Py-Authorize Documentation',
192 | u'Vincent Catalano', 'manual'),
193 | ]
194 |
195 | # The name of an image file (relative to this directory) to place at the top of
196 | # the title page.
197 | #latex_logo = None
198 |
199 | # For "manual" documents, if this is true, then toplevel headings are parts,
200 | # not chapters.
201 | #latex_use_parts = False
202 |
203 | # If true, show page references after internal links.
204 | #latex_show_pagerefs = False
205 |
206 | # If true, show URL addresses after external links.
207 | #latex_show_urls = False
208 |
209 | # Documents to append as an appendix to all manuals.
210 | #latex_appendices = []
211 |
212 | # If false, no module index is generated.
213 | #latex_domain_indices = True
214 |
215 |
216 | # -- Options for manual page output --------------------------------------------
217 |
218 | # One entry per manual page. List of tuples
219 | # (source start file, name, description, authors, manual section).
220 | man_pages = [
221 | ('index', 'py-authorize', u'Py-Authorize Documentation',
222 | [u'Vincent Catalano'], 1)
223 | ]
224 |
225 | # If true, show URL addresses after external links.
226 | #man_show_urls = False
227 |
228 |
229 | # -- Options for Texinfo output ------------------------------------------------
230 |
231 | # Grouping the document tree into Texinfo files. List of tuples
232 | # (source start file, target name, title, author,
233 | # dir menu entry, description, category)
234 | texinfo_documents = [
235 | ('index', 'Py-Authorize', u'Py-Authorize Documentation',
236 | u'Vincent Catalano', 'Py-Authorize', 'One line description of project.',
237 | 'Miscellaneous'),
238 | ]
239 |
240 | # Documents to append as an appendix to all manuals.
241 | #texinfo_appendices = []
242 |
243 | # If false, no module index is generated.
244 | #texinfo_domain_indices = True
245 |
246 | # How to display URL addresses: 'footnote', 'no', or 'inline'.
247 | #texinfo_show_urls = 'footnote'
248 |
249 | # If true, do not generate a @detailmenu in the "Top" node's menu.
250 | #texinfo_no_detailmenu = False
251 |
--------------------------------------------------------------------------------
/tests/test_recurring_api.py:
--------------------------------------------------------------------------------
1 | from authorize import Configuration
2 | from authorize.xml_data import prettify
3 |
4 | from datetime import date
5 |
6 | from unittest import TestCase
7 |
8 |
9 | CREATE_RECURRING = {
10 | 'name': 'Ultimate Robot Supreme Plan',
11 | 'amount': 40.00,
12 | 'total_occurrences': 30,
13 | 'start_date': date.today().isoformat(),
14 | 'interval_length': 2,
15 | 'interval_unit': 'months',
16 | 'trial_amount': 30.00,
17 | 'trial_occurrences': 2,
18 | 'credit_card': {
19 | 'card_number': '4111111111111111',
20 | 'expiration_month': '04',
21 | 'expiration_year': '2014',
22 | 'card_code': '456',
23 | },
24 | 'billing': {
25 | 'first_name': 'Rob',
26 | 'last_name': 'Oteron',
27 | 'company': 'Robotron Studios',
28 | 'address': '101 Computer Street',
29 | 'city': 'Tucson',
30 | 'state': 'AZ',
31 | 'zip': '85704',
32 | 'country': 'US',
33 | },
34 | 'order': {
35 | 'invoice_number': 'INV0001',
36 | 'description': 'Just another invoice...',
37 | },
38 | 'customer': {
39 | 'merchant_id': '1234567890',
40 | 'email': 'rob@robotronstudios.com',
41 | },
42 | 'shipping': {
43 | 'first_name': 'Rob',
44 | 'last_name': 'Oteron',
45 | 'company': 'Robotron Studios',
46 | 'address': '101 Computer Street',
47 | 'city': 'Tucson',
48 | 'state': 'AZ',
49 | 'zip': '85704',
50 | 'country': 'US',
51 | },
52 | }
53 |
54 | UPDATE_RECURRING = {
55 | 'name': 'Ultimate Robot Supreme Plan',
56 | 'amount': 40.00,
57 | 'total_occurrences': 30,
58 | 'start_date': date.today().isoformat(),
59 | 'trial_amount': 30.00,
60 | 'trial_occurrences': 2,
61 | 'credit_card': {
62 | 'card_number': '4111111111111111',
63 | 'expiration_month': '04',
64 | 'expiration_year': '2014',
65 | 'card_code': '456',
66 | },
67 | 'billing': {
68 | 'first_name': 'Rob',
69 | 'last_name': 'Oteron',
70 | 'company': 'Robotron Studios',
71 | 'address': '101 Computer Street',
72 | 'city': 'Tucson',
73 | 'state': 'AZ',
74 | 'zip': '85704',
75 | 'country': 'US',
76 | },
77 | 'order': {
78 | 'invoice_number': 'INV0001',
79 | 'description': 'Just another invoice...',
80 | },
81 | 'customer': {
82 | 'merchant_id': '1234567890',
83 | 'email': 'rob@robotronstudios.com',
84 | },
85 | 'shipping': {
86 | 'first_name': 'Rob',
87 | 'last_name': 'Oteron',
88 | 'company': 'Robotron Studios',
89 | 'address': '101 Computer Street',
90 | 'city': 'Tucson',
91 | 'state': 'AZ',
92 | 'zip': '85704',
93 | 'country': 'US',
94 | },
95 | }
96 |
97 | UPDATE_RECURRING_NO_PAYMENT = UPDATE_RECURRING.copy()
98 | del UPDATE_RECURRING_NO_PAYMENT['credit_card']
99 |
100 | UPDATE_RECURRING_PAYMENT_ONLY = {
101 | 'credit_card': {
102 | 'card_number': '4111111111111111',
103 | 'expiration_month': '04',
104 | 'expiration_year': '2014',
105 | 'card_code': '456',
106 | },
107 | }
108 |
109 | CREATE_RECURRING_REQUEST = '''
110 |
111 |
112 |
113 | 8s8tVnG5t
114 | 5GK7mncw8mG2946z
115 |
116 |
117 | Ultimate Robot Supreme Plan
118 |
119 |
120 | 2
121 | months
122 |
123 | {0}
124 | 30
125 | 2
126 |
127 | 40.00
128 | 30.00
129 |
130 |
131 | 4111111111111111
132 | 2014-04
133 | 456
134 |
135 |
136 |
137 | INV0001
138 | Just another invoice...
139 |
140 |
141 | 1234567890
142 | rob@robotronstudios.com
143 |
144 |
145 | Rob
146 | Oteron
147 | Robotron Studios
148 | 101 Computer Street
149 | Tucson
150 | AZ
151 | 85704
152 | US
153 |
154 |
155 | Rob
156 | Oteron
157 | Robotron Studios
158 | 101 Computer Street
159 | Tucson
160 | AZ
161 | 85704
162 | US
163 |
164 |
165 |
166 | '''.format(date.today().isoformat())
167 |
168 | DETAILS_RECURRING_REQUEST = '''
169 |
170 |
171 |
172 | 8s8tVnG5t
173 | 5GK7mncw8mG2946z
174 |
175 | 0932576929034
176 |
177 | '''
178 |
179 | STATUS_RECURRING_REQUEST = '''
180 |
181 |
182 |
183 | 8s8tVnG5t
184 | 5GK7mncw8mG2946z
185 |
186 | 0932576929034
187 |
188 | '''
189 |
190 | UPDATE_RECURRING_REQUEST = '''
191 |
192 |
193 |
194 | 8s8tVnG5t
195 | 5GK7mncw8mG2946z
196 |
197 | 0932576929034
198 |
199 | Ultimate Robot Supreme Plan
200 |
201 | {0}
202 | 30
203 | 2
204 |
205 | 40.00
206 | 30.00
207 |
208 |
209 | 4111111111111111
210 | 2014-04
211 | 456
212 |
213 |
214 |
215 | INV0001
216 | Just another invoice...
217 |
218 |
219 | 1234567890
220 | rob@robotronstudios.com
221 |
222 |
223 | Rob
224 | Oteron
225 | Robotron Studios
226 | 101 Computer Street
227 | Tucson
228 | AZ
229 | 85704
230 | US
231 |
232 |
233 | Rob
234 | Oteron
235 | Robotron Studios
236 | 101 Computer Street
237 | Tucson
238 | AZ
239 | 85704
240 | US
241 |
242 |
243 |
244 | '''.format(date.today().isoformat())
245 |
246 | UPDATE_RECURRING_NO_PAYMENT_REQUEST = '''
247 |
248 |
249 |
250 | 8s8tVnG5t
251 | 5GK7mncw8mG2946z
252 |
253 | 0932576929034
254 |
255 | Ultimate Robot Supreme Plan
256 |
257 | {0}
258 | 30
259 | 2
260 |
261 | 40.00
262 | 30.00
263 |
264 | INV0001
265 | Just another invoice...
266 |
267 |
268 | 1234567890
269 | rob@robotronstudios.com
270 |
271 |
272 | Rob
273 | Oteron
274 | Robotron Studios
275 | 101 Computer Street
276 | Tucson
277 | AZ
278 | 85704
279 | US
280 |
281 |
282 | Rob
283 | Oteron
284 | Robotron Studios
285 | 101 Computer Street
286 | Tucson
287 | AZ
288 | 85704
289 | US
290 |
291 |
292 |
293 | '''.format(date.today().isoformat())
294 |
295 | UPDATE_RECURRING_PAYMENT_ONLY_REQUEST = '''
296 |
297 |
298 |
299 | 8s8tVnG5t
300 | 5GK7mncw8mG2946z
301 |
302 | 0932576929034
303 |
304 |
305 |
306 |
307 | 4111111111111111
308 | 2014-04
309 | 456
310 |
311 |
312 |
313 |
314 | '''
315 |
316 | DELETE_RECURRING_REQUEST = '''
317 |
318 |
319 |
320 | 8s8tVnG5t
321 | 5GK7mncw8mG2946z
322 |
323 | 0932576929034
324 |
325 | '''
326 |
327 |
328 | class RecurringAPITests(TestCase):
329 |
330 | maxDiff = None
331 |
332 | def test_create_recurring_request(self):
333 | request_xml = Configuration.api.recurring._create_request(CREATE_RECURRING)
334 | request_string = prettify(request_xml)
335 | self.assertEqual(request_string, CREATE_RECURRING_REQUEST.strip())
336 |
337 | def test_details_recurring_request(self):
338 | request_xml = Configuration.api.recurring._details_request('0932576929034')
339 | request_string = prettify(request_xml)
340 | self.assertEqual(request_string, DETAILS_RECURRING_REQUEST.strip())
341 |
342 | def test_status_recurring_request(self):
343 | request_xml = Configuration.api.recurring._status_request('0932576929034')
344 | request_string = prettify(request_xml)
345 | self.assertEqual(request_string, STATUS_RECURRING_REQUEST.strip())
346 |
347 | def test_update_recurring_request(self):
348 | request_xml = Configuration.api.recurring._update_request('0932576929034', UPDATE_RECURRING)
349 | request_string = prettify(request_xml)
350 | self.assertEqual(request_string, UPDATE_RECURRING_REQUEST.strip())
351 |
352 | request_xml = Configuration.api.recurring._update_request('0932576929034', UPDATE_RECURRING_PAYMENT_ONLY)
353 | request_string = prettify(request_xml)
354 | self.assertEqual(request_string, UPDATE_RECURRING_PAYMENT_ONLY_REQUEST.strip())
355 |
356 | request_xml = Configuration.api.recurring._update_request('0932576929034', UPDATE_RECURRING_NO_PAYMENT)
357 | request_string = prettify(request_xml)
358 | self.assertEqual(request_string, UPDATE_RECURRING_NO_PAYMENT_REQUEST.strip())
359 |
360 | def test_delete_recurring_request(self):
361 | request_xml = Configuration.api.recurring._delete_request('0932576929034')
362 | request_string = prettify(request_xml)
363 | self.assertEqual(request_string, DELETE_RECURRING_REQUEST.strip())
364 |
--------------------------------------------------------------------------------
/docs/transaction.rst:
--------------------------------------------------------------------------------
1 | Transactions
2 | ============
3 |
4 | The primary purpose for any payment gateway is to provide functionality for
5 | taking payments for goods or services and charging a consumer. Py-Authorize's
6 | Transaction API provides all the functionality developers will need for all
7 | situations when developing a payment system.
8 |
9 | Sale
10 | ----
11 |
12 | The most common transaction type for credit cards is a ''sale''. During the
13 | transaction, the credit card is first authorized for the given transaction
14 | amount, if approved, it is automattically submitted for settlement.
15 |
16 | .. note::
17 |
18 | When performing a ``sale`` transaction, Py-Authorize is actually
19 | performing an ``authCapture``.
20 |
21 | Minimal Example
22 | ~~~~~~~~~~~~~~~
23 |
24 | .. code-block:: python
25 |
26 | result = authorize.Transaction.sale({
27 | 'amount': 40.00,
28 | 'credit_card': {
29 | 'card_number': '4111111111111111',
30 | 'expiration_date': '04/2014',
31 | }
32 | })
33 |
34 | result.transaction_response.trans_id
35 | # e.g. '2194343352'
36 |
37 |
38 | Py-Authorize fully supports all Authorize.net gateway parameters for
39 | transactions.
40 |
41 | Full Example
42 | ~~~~~~~~~~~~
43 |
44 | .. code-block:: python
45 |
46 | result = authorize.Transaction.sale({
47 | 'amount': 56.00,
48 | 'email': 'rob@robotronstudios.com',
49 | 'credit_card': {
50 | 'card_number': '4111111111111111',
51 | 'card_code': '523',
52 | 'expiration_date': '04/2014',
53 | },
54 | 'shipping': {
55 | 'first_name': 'Rob',
56 | 'last_name': 'Oteron',
57 | 'company': 'Robotron Studios',
58 | 'address': '101 Computer Street',
59 | 'city': 'Tucson',
60 | 'state': 'AZ',
61 | 'zip': '85704',
62 | 'country': 'US',
63 | },
64 | 'billing': {
65 | 'first_name': 'Rob',
66 | 'last_name': 'Oteron',
67 | 'company': 'Robotron Studios',
68 | 'address': '101 Computer Street',
69 | 'city': 'Tucson',
70 | 'state': 'AZ',
71 | 'zip': '85704',
72 | 'country': 'US',
73 | 'phone_number': '520-123-4567',
74 | 'fax_number': '520-456-7890',
75 | },
76 | 'tax': {
77 | 'amount': 4.00,
78 | 'name': 'Double Taxation Tax',
79 | 'description': 'Another tax for paying double tax',
80 | },
81 | 'duty': {
82 | 'amount': 2.00,
83 | 'name': 'The amount for duty',
84 | 'description': 'I can''t believe you would pay for duty',
85 | },
86 | 'line_items': [{
87 | 'item_id': 'CIR0001',
88 | 'name': 'Circuit Board',
89 | 'description': 'A brand new robot component',
90 | 'quantity': 5,
91 | 'unit_price': 4.00,
92 | 'taxable': 'true',
93 | }, {
94 | 'item_id': 'CIR0002',
95 | 'name': 'Circuit Board 2.0',
96 | 'description': 'Another new robot component',
97 | 'quantity': 1,
98 | 'unit_price': 10.00,
99 | 'taxable': 'true',
100 | }, {
101 | 'item_id': 'SCRDRVR',
102 | 'name': 'Screwdriver',
103 | 'description': 'A basic screwdriver',
104 | 'quantity': 1,
105 | 'unit_price': 10.00,
106 | 'taxable': 'true',
107 | }],
108 | 'user_fields': [{
109 | 'name': 'additionalDescription',
110 | 'value': 'An additional description goes here...'
111 | }, {
112 | 'name': 'moreInfo',
113 | 'value': 'This is some more information...'
114 | }]
115 | 'order': {
116 | 'invoice_number': 'INV0001',
117 | 'description': 'Just another invoice...',
118 | },
119 | 'shipping_and_handling': {
120 | 'amount': 10.00,
121 | 'name': 'UPS 2-Day Shipping',
122 | 'description': 'Handle with care',
123 | },
124 | 'extra_options': {
125 | 'customer_ip': '100.0.0.1',
126 | },
127 | 'retail': {
128 | 'market_type':0,
129 | 'device_type':7,
130 | },
131 | 'tax_exempt': False,
132 | 'recurring': True,
133 | 'transaction_settings': {
134 | 'duplicate_window': 120,
135 | },
136 | })
137 |
138 | result.transaction_response.trans_id
139 | # e.g. '2194343353'
140 |
141 |
142 | Card Present Example
143 | ~~~~~~~~~~~~~~~~~~~~
144 |
145 | If doing a card present transaction, track data can be passed in instead of a parsed credit card.
146 |
147 | .. note::
148 |
149 | It may still be useful to parse the track data in application logic to verify expiration date or card issuer.
150 |
151 | .. code-block:: python
152 |
153 | result = authorize.Transaction.sale({
154 | 'amount': 40.00,
155 | 'track_data': {
156 | 'track_1': '%B4111111111111111^OTERON/ROB^14041010300523300000000000000000000000000000000?',
157 | }
158 | })
159 |
160 | result.transaction_response.trans_id
161 | # e.g. '2194343352'
162 |
163 |
164 | Minimal Bank Account Transaction
165 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
166 |
167 | Transactions can also be ran against bank accounts.
168 |
169 | .. warning::
170 |
171 | Since bank account (eCheck.net) transactions are handled differently from
172 | credit card transactions, you should avoid using the `auth` method when
173 | dealing with bank accounts. Only use the `sale` method when processing
174 | payments.
175 |
176 | .. code-block:: python
177 |
178 | result = authorize.Transaction.sale({
179 | 'amount': 40.00,
180 | 'bank_account': {
181 | 'routing_number': '322271627',
182 | 'account_number': '00987467838473',
183 | 'name_on_account': 'Rob Otron',
184 | },
185 | })
186 |
187 | result.transaction_response.trans_id
188 | # e.g. '2194343357'
189 |
190 |
191 | Full Transactions with Bank Accounts
192 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
193 |
194 | .. code-block:: python
195 |
196 | result = authorize.Transaction.sale({
197 | 'amount': 56.00,
198 | 'email': 'rob@robotronstudios.com',
199 | 'bank_account': {
200 | 'customer_type': 'individual',
201 | 'account_type': 'checking',
202 | 'routing_number': '322271627',
203 | 'account_number': '00987467838473',
204 | 'name_on_account': 'Rob Otron',
205 | 'bank_name': 'Evil Bank Co.',
206 | 'echeck_type': 'WEB',
207 | },
208 | 'shipping': {
209 | 'first_name': 'Rob',
210 | 'last_name': 'Oteron',
211 | 'company': 'Robotron Studios',
212 | 'address': '101 Computer Street',
213 | 'city': 'Tucson',
214 | 'state': 'AZ',
215 | 'zip': '85704',
216 | 'country': 'US',
217 | },
218 | 'billing': {
219 | 'first_name': 'Rob',
220 | 'last_name': 'Oteron',
221 | 'company': 'Robotron Studios',
222 | 'address': '101 Computer Street',
223 | 'city': 'Tucson',
224 | 'state': 'AZ',
225 | 'zip': '85704',
226 | 'country': 'US',
227 | 'phone_number': '520-123-4567',
228 | 'fax_number': '520-456-7890',
229 | },
230 | 'tax': {
231 | 'amount': 4.00,
232 | 'name': 'Double Taxation Tax',
233 | 'description': 'Another tax for paying double tax',
234 | },
235 | 'duty': {
236 | 'amount': 2.00,
237 | 'name': 'The amount for duty',
238 | 'description': 'I can''t believe you would pay for duty',
239 | },
240 | 'line_items': [{
241 | 'item_id': 'CIR0001',
242 | 'name': 'Circuit Board',
243 | 'description': 'A brand new robot component',
244 | 'quantity': 5,
245 | 'unit_price': 4.00,
246 | 'taxable': 'true',
247 | }, {
248 | 'item_id': 'CIR0002',
249 | 'name': 'Circuit Board 2.0',
250 | 'description': 'Another new robot component',
251 | 'quantity': 1,
252 | 'unit_price': 10.00,
253 | 'taxable': 'true',
254 | }, {
255 | 'item_id': 'SCRDRVR',
256 | 'name': 'Screwdriver',
257 | 'description': 'A basic screwdriver',
258 | 'quantity': 1,
259 | 'unit_price': 10.00,
260 | 'taxable': 'true',
261 | }],
262 | 'order': {
263 | 'invoice_number': 'INV0001',
264 | 'description': 'Just another invoice...',
265 | },
266 | 'shipping_and_handling': {
267 | 'amount': 10.00,
268 | 'name': 'UPS 2-Day Shipping',
269 | 'description': 'Handle with care',
270 | },
271 | 'extra_options': {
272 | 'customer_ip': '100.0.0.1',
273 | },
274 | 'tax_exempt': False,
275 | 'recurring': True,
276 | 'transaction_settings': {
277 | 'duplicate_window': 120,
278 | },
279 | })
280 |
281 | result.transaction_response.trans_id
282 | # e.g. '2194343358'
283 |
284 |
285 | Transactions with CIM Data
286 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
287 |
288 | Transactions can also be ran with stored customer payment profile
289 | information. When performing a transaction for a CIM managed payment profile,
290 | you must include the customer ID and payment profile ID. Additionally, you
291 | can include a customer's stored address ID as the shipping address for an
292 | order.
293 |
294 | .. code-block:: python
295 |
296 | result = authorize.Transaction.sale({
297 | 'amount': 56.00,
298 | 'customer_id': '19086684',
299 | 'payment_id': '17633614',
300 | 'address_id': '14634122',
301 | })
302 |
303 | result.transaction_response.trans_id
304 | # e.g. '2194343354'
305 |
306 |
307 | Full Transactions Example with CIM Data
308 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
309 |
310 | .. code-block:: python
311 |
312 | result = authorize.Transaction.sale({
313 | 'amount': 56.00,
314 | 'customer_id': '19086684',
315 | 'payment_id': '17633614',
316 | 'address_id': '14634122',
317 | 'tax': {
318 | 'amount': 4.00,
319 | 'name': 'Double Taxation Tax',
320 | 'description': 'Another tax for paying double tax',
321 | },
322 | 'duty': {
323 | 'amount': 2.00,
324 | 'name': 'The amount for duty',
325 | 'description': 'I can''t believe you would pay for duty',
326 | },
327 | 'line_items': [{
328 | 'item_id': 'CIR0001',
329 | 'name': 'Circuit Board',
330 | 'description': 'A brand new robot component',
331 | 'quantity': 5,
332 | 'unit_price': 4.00,
333 | 'taxable': 'true',
334 | }, {
335 | 'item_id': 'CIR0002',
336 | 'name': 'Circuit Board 2.0',
337 | 'description': 'Another new robot component',
338 | 'quantity': 1,
339 | 'unit_price': 10.00,
340 | 'taxable': 'true',
341 | }, {
342 | 'item_id': 'SCRDRVR',
343 | 'name': 'Screwdriver',
344 | 'description': 'A basic screwdriver',
345 | 'quantity': 1,
346 | 'unit_price': 10.00,
347 | 'taxable': 'true',
348 | }],
349 | 'order': {
350 | 'invoice_number': 'INV0001',
351 | 'description': 'Just another invoice...',
352 | 'order_number': 'PONUM00001',
353 | },
354 | 'shipping_and_handling': {
355 | 'amount': 10.00,
356 | 'name': 'UPS 2-Day Shipping',
357 | 'description': 'Handle with care',
358 | },
359 | 'extra_options': {
360 | 'customer_ip': '100.0.0.1',
361 | },
362 | 'tax_exempt': False,,
363 | 'recurring': True,
364 | 'transaction_settings': {
365 | 'duplicate_window': 120,
366 | },
367 | })
368 |
369 | result.transaction_response.trans_id
370 | # e.g. '2194343355'
371 |
372 |
373 | .. note::
374 |
375 | The `email` field cannot be used in combination with the `customer_id`
376 | field. If the `customer_id` field is provided, the `email` field will
377 | be ignored during the transaction processing.
378 |
379 | Auth
380 | ----
381 |
382 | The ``auth`` method is equivalent to the the Authorize.net ``authorizeOnly``
383 | method. When calling ``auth``, the credit card is temporarily authorized for
384 | the given transaction amount without being submitted for settlement. This
385 | allows you to ensure you will be able to charge the card but hold off if in
386 | case you later no longer need to charge the card or need reduce the amount
387 | you plan to charge. In order to finalize the transaction charge, you must
388 | settle the transaction by using the ``settle`` transaction method.
389 |
390 | This method takes the same parameters as the ``sale`` method.
391 |
392 | Example
393 | ~~~~~~~
394 |
395 | .. code-block:: python
396 |
397 | result = authorize.Transaction.auth({
398 | 'amount': 40.00,
399 | 'credit_card': {
400 | 'card_number': '4111111111111111',
401 | 'expiration_date': '04/2014,
402 | }
403 | })
404 |
405 | result.transaction_response.trans_id
406 | # e.g. '2194343356'
407 |
408 | The ``auth`` method takes the same values as as the ``sale`` method.
409 |
410 | Settling
411 | --------
412 |
413 | In order to finalize a previously authorized transaction, you must call the
414 | ``settle`` method with the transaction ID. When settling a transaction, the
415 | amount for the transaction can be changed as long as it is less than the
416 | original authorized amount.
417 |
418 | Example
419 | ~~~~~~~
420 |
421 | .. code-block:: python
422 |
423 | result = authorize.Transaction.settle('89798235')
424 |
425 | The amount is not required if you want to settle the authorized amount. To
426 | settle a different amount, pass the amout as the second parameter.
427 |
428 | .. code-block:: python
429 |
430 | result = authorize.Transaction.settle('89798235', 20.00)
431 |
432 | Refund
433 | ------
434 |
435 | This transaction type is used to refund a customer for a transaction that was
436 | originally processed and successfully settled through the payment gateway (it
437 | is the Authorize.net equivalent of a Credit).
438 |
439 | When issuing a refund, Authorize.net requires the amount of the transaction,
440 | the last four digits of the credit card and the transaction ID. If you do not
441 | have the amount or last four digits of the credit card readily available,
442 | this information can be gotten using the ``details`` method.
443 |
444 | Example
445 | ~~~~~~~
446 |
447 | .. code-block:: python
448 |
449 | result = authorize.Transaction.refund({
450 | 'amount': 40.00,
451 | 'last_four': '1111',
452 | 'transaction_id': '0123456789',
453 | 'order': {
454 | 'invoice_number': 'INV0001',
455 | 'description': 'Just another invoice...',
456 | 'order_number': 'PONUM00001',
457 | }
458 | })
459 |
460 |
461 | .. _void:
462 |
463 | Void
464 | ----
465 |
466 | This transaction type can be used to cancel either an original transaction
467 | that is not yet settled or an entire order composed of more than one
468 | transaction. A Void prevents the transaction or the order from being sent
469 | for settlement. You will only be able to void a transaction that is not
470 | already settled, expired, or failed.
471 |
472 | Example
473 | ~~~~~~~
474 |
475 | .. code-block:: python
476 |
477 | result = authorize.Transaction.void('0123456789')
478 |
479 |
480 | Credit
481 | ------
482 |
483 | Authorize.net provides the ability to issue refunds for transactions that
484 | were not originally submitted through the payment gateway (it is the
485 | Authorize.net equivalent of an Unlinked Credit). It also allows you to
486 | override restrictions set on basic credits, such as refunds for transactions
487 | beyond the 120-day refund limit.
488 |
489 | .. note::
490 |
491 | The ability to submit unlinked credits is not a standard payment
492 | gateway account feature. You must request the Expanded Credits
493 | Capability (ECC) feature by submitting an application to Authorize.net.
494 | More information on Unlinked Credits can be found under `Authorize.net
495 | Transaction Types`_ documentation.
496 |
497 | Example
498 | ~~~~~~~
499 |
500 | .. code-block:: python
501 |
502 | result = authorize.Transaction.credit({
503 | 'amount': 120.00,
504 | 'customer_id': '0987654321',
505 | 'payment_id': '1348979152'
506 | })
507 |
508 |
509 | .. _Authorize.net Transaction Types: http://www.authorize.net/support/merchant/Submitting_Transactions/Credit_Card_Transaction_Types.htm#Unlinked
510 |
511 |
512 | Details
513 | -------
514 |
515 | This transaction type is used to get detailed information about one specific
516 | transaction based on the transaction ID.
517 |
518 | Example
519 | ~~~~~~~
520 |
521 | .. code-block:: python
522 |
523 | result = authorize.Transaction.details('0123456789')
524 |
525 |
526 | List Transactions
527 | -----------------
528 |
529 | This transaction type will return data for all transactions in a given batch.
530 |
531 | Example
532 | ~~~~~~~
533 |
534 | .. code-block:: python
535 |
536 | result = authorize.Transaction.list('0123456789')
537 |
538 |
539 | Additionally, omitting the batch ID will return data for all transactions
540 | that are currently unsettled.
541 |
542 | Example
543 | ~~~~~~~
544 |
545 | .. code-block:: python
546 |
547 | result = authorize.Transaction.list()
548 |
549 |
--------------------------------------------------------------------------------