├── 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 | --------------------------------------------------------------------------------