├── LICENSE ├── README ├── payment_processor ├── __init__.py ├── exceptions │ └── __init__.py ├── gateways │ ├── __init__.py │ ├── authorizenet.py │ ├── dummygateway.py │ └── nationalprocessing.py └── methods │ └── __init__.py └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Ian Halpern 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Python Payment Processor - (c) RentShare Inc 2012, Author: Ian Halpern 2 | 3 | Python Payment Processor is a library for python providing a simple abstraction around various payment methods and payment gateways. It includes support for standard gateways such as Authorize.net and is always working to expand it's supported gateway repertoire. Python Payment is currently being used in production systems transferring hundreds of thousands of dollars a month. 4 | 5 | Python Payment Processor is released under the MIT license. 6 | 7 | *A special thanks to Brandon Stoner for collaborating on the initial concept design. 8 | 9 | INSTALL: 10 | 11 | To install python-payment-processor run: 12 | 13 | $ sudo ./setup.py install 14 | 15 | HOW TO: 16 | 17 | Here is a simple example using the authroize.net payment gateway. This example 18 | creates a authorize.net payment gateway and a credit card transaction and 19 | processes the transaction. 20 | 21 | ---------------------------------------------------- 22 | import payment_processor 23 | from payment_processor.exceptions import * 24 | 25 | # the authorize.net gateway requires valid authorize.net 'login' and 'trans_key' variables 26 | gateway = payment_processor.Gateway( 'authorize.net', login='XXX', trans_key='XXX' ) 27 | 28 | # other authroize.net variables include 'use_test_url' which, if set, 29 | # will use https://test.authorize.net/ instead of https://secure.authorize.net 30 | # if you are using a developer account 31 | 32 | card = payment_processor.methods.CreditCard( 33 | card_number=4011111111111111, 34 | expiration_date=datetime.datetime( 2014, 1, 1 ), 35 | first_name='First', 36 | last_name='Last', 37 | zip_code='10001', 38 | address='1 Somewhere Ave', 39 | city='New York', 40 | state='NY' 41 | ) 42 | 43 | payment = payment_processor.PaymentInfo( 44 | amount = 20, 45 | customer_id = 1, 46 | order_number = '43DJ-7203-D897-SS97', 47 | ship_first_name = 'First', 48 | ship_last_name = 'Last', 49 | ship_address = '1 Somewhere Ave', 50 | ship_city = 'New York', 51 | ship_state = 'NY', 52 | ship_email = 'email@example.com', 53 | ship_phone = '222-333-4444', 54 | ip = '65.192.14.10', 55 | description = 'Some Order' 56 | ) 57 | 58 | t = payment_processor.Transaction( payment=payment, method=card, gateway=gateway ) 59 | 60 | try: 61 | t.process() 62 | except TransactionDeclined: 63 | # The transaction requested was declined for such reasons as insufficient funds or flagged for fraud. 64 | raise 65 | except InvalidCardNumber: 66 | # The credit card number provided was invalid. 67 | raise 68 | except InvalidCardExpirationDate: 69 | # The credit card expiration date provided was invalid. 70 | raise 71 | except InvalidCardCode: 72 | # The credit card code provided was invalid. 73 | raise 74 | except InvalidRoutingNumber: 75 | # The routing number provided was invalid (only applicable to Check methods). 76 | raise 77 | except InvalidAccountNumber: 78 | # The account number provided was invalid (only applicable to Check methods). 79 | raise 80 | except InvalidBillingAddress: 81 | # The billing address provided was invalid. 82 | raise 83 | except InvalidBillingZipcode: 84 | # The billing zipcode provided was invalid. 85 | raise 86 | except TransactionAmountLimitExceeded: 87 | # The per-transaction limit was exceeded. 88 | raise 89 | except TransactionFailed: 90 | # The transaction failed for other reasons usually relating using python-payment in ways unsupported by the gateway. 91 | raise 92 | 93 | ---------------------------------------------------- 94 | 95 | Using payment_processor.Gateway always take a string as the first argument which is used as 96 | a simple way to dynamically load a specific gateway found in payment.gateways. 97 | 98 | You can easily create a gateway outside of the python-payment-processor module, just create a 99 | class that overloads payment.gateways.GenericGateway. 100 | 101 | ---------------------------------------------------- 102 | 103 | from payment_processor.gateways import GenericGateway 104 | 105 | class MyGateway( GenericGateway ) 106 | ... 107 | -------------------------------------------------------------------------------- /payment_processor/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | from payment_processor.exceptions import * 3 | from payment_processor import methods 4 | 5 | class Gateway( object ): 6 | '''The Gateway class is a wrapper around the gateways found in payment_processor.gateways. It should be 7 | called with the name of the gateway as the first paramater. example: Gateway( 'authorize.net' )''' 8 | 9 | gateway = None 10 | 11 | def __init__( self, gateway_name, *args, **kwargs ): 12 | gateway_classname = ''.join([ s.capitalize() for s in re.split( '\W', gateway_name ) ]) 13 | gateway_modulename = gateway_classname.lower() 14 | 15 | try: 16 | module = __import__( 'payment_processor.gateways.%s' % gateway_modulename, fromlist=[ gateway_modulename ] ) 17 | self.gateway = getattr( module, gateway_classname )( *args, **kwargs ) 18 | except ImportError, AttributeError: 19 | raise NoGatewayError( "No gateway by the name '%s' exists." % gateway_name ) 20 | 21 | def __getattr__( self, value ): 22 | return getattr( self.__dict__['gateway'], value ) 23 | 24 | class PaymentInfo( object ): 25 | amount = None 26 | order_number = None 27 | customer_id = None 28 | description = None 29 | ip = None 30 | ship_first_name = None 31 | ship_last_name = None 32 | ship_company = None 33 | ship_address = None 34 | ship_address2 = None 35 | ship_city = None 36 | ship_state = None 37 | ship_zip_code = None 38 | ship_country = None 39 | ship_email = None 40 | ship_phone = None 41 | 42 | def __init__( self, amount=None, order_number = None, customer_id = None, description = None, ship_first_name = None, ship_company=None, 43 | ship_last_name = None, ship_address = None, ship_address2 = None, ship_city = None, ship_state = None, 44 | ship_zip_code = None, ship_country = None, ship_email = None, ship_phone = None, ip = None ): 45 | 46 | self.amount = amount 47 | self.order_number = order_number 48 | self.customer_id = customer_id 49 | self.description = description 50 | self.ip = ip 51 | self.ship_first_name = ship_first_name 52 | self.ship_last_name = ship_last_name 53 | self.ship_address = ship_address 54 | self.ship_city = ship_city 55 | self.ship_state = ship_state 56 | self.ship_zip_code = ship_zip_code 57 | self.ship_country = ship_country 58 | self.ship_email = ship_email 59 | self.ship_phone = ship_phone 60 | 61 | class Transaction( object ): 62 | 63 | PENDING = 'pending' 64 | AUTHORIZED = 'authorized' 65 | CAPTURED = 'captured' 66 | VOIDED = 'voided' 67 | REFUNDED = 'refunded' 68 | 69 | status = None 70 | status_pending = None 71 | 72 | payment = None 73 | method = None 74 | gateway = None 75 | 76 | trans_id = None 77 | last_response = None 78 | last_response_text = None 79 | 80 | api = None 81 | 82 | def __init__( self, payment, method, gateway, status=PENDING, trans_id=None, api=None ): 83 | self.payment = payment 84 | self.method = method 85 | self.gateway = gateway 86 | 87 | self.status = status 88 | self.trans_id = trans_id 89 | 90 | self.api = api or {} 91 | 92 | def handleResponse( self ): 93 | self.gateway.handleResponse( self ) 94 | self.status = self.status_pending 95 | 96 | def mergeAPI( self, api ): 97 | if api: 98 | api.update( self.api ) 99 | else: 100 | api = self.api 101 | return api 102 | 103 | # Authorize and capture a sale 104 | def process( self, api=None ): 105 | self.status_pending = Transaction.CAPTURED 106 | self.gateway.process( self, self.mergeAPI( api ) ) 107 | 108 | # Authorize a sale 109 | def authorize( self, api=None ): 110 | self.status_pending = Transaction.AUTHORIZED 111 | self.gateway.authorize( self, self.mergeAPI( api ) ) 112 | 113 | # Captures funds from a successful authorization 114 | def capture( self, api=None ): 115 | self.status_pending = Transaction.CAPTURED 116 | self.gateway.capture( self, self.mergeAPI( api ) ) 117 | 118 | # Void a sale 119 | def void( self, api=None ): 120 | self.status_pending = Transaction.VOIDED 121 | 122 | self.gateway.void( self, self.mergeAPI( api ) ) 123 | 124 | # Refund a processed transaction 125 | def refund( self, api=None ): 126 | self.status_pending = Transaction.REFUNDED 127 | self.gateway.refund( self, self.mergeAPI( api ) ) 128 | 129 | # Updates the order information for the given transaction 130 | def update( self, api=None ): 131 | self.gateway.update( self, self.mergeAPI( api ) ) 132 | -------------------------------------------------------------------------------- /payment_processor/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | class Error( Exception ): 2 | def __init__( self, reason, **kwargs ): 3 | for key, val in kwargs.items(): setattr( self, key, val ) 4 | Exception.__init__( self, reason ) 5 | 6 | class NoGatewayError( Error ): 7 | """Exception raised when the specified gateway does not exist""" 8 | pass 9 | 10 | class TransactionAmountLimitExceeded( Error ): 11 | pass 12 | 13 | class PaymentMethodUnsupportedByGateway( Error ): 14 | """Exception raised when the transaction method is unsupported by the gateway""" 15 | pass 16 | 17 | class TransactionFailed( Error ): 18 | """Explains any undefined error when processing a transaction""" 19 | pass 20 | 21 | class TransactionDeclined( Error ): 22 | """Raised when a transaction was declined though all fields were entered correctly, usually a result of insufficient funds.""" 23 | pass 24 | 25 | # All specific errors should be restricted to invalid user inputted fields 26 | # that the developer cannot check beforehand like credit card number 27 | # any other errors will be lumped into TransactionFailed 28 | 29 | class InvalidCardNumber( Error ): 30 | pass 31 | 32 | class InvalidCardExpirationDate( Error ): 33 | pass 34 | 35 | class InvalidCardCode( Error ): 36 | pass 37 | 38 | class InvalidRoutingNumber( Error ): 39 | pass 40 | 41 | class InvalidAccountNumber( Error ): 42 | pass 43 | 44 | class InvalidBillingAddress( Error ): 45 | pass 46 | 47 | class InvalidBillingZipcode( Error ): 48 | pass 49 | 50 | class DuplicateTransaction( Error ): 51 | pass 52 | 53 | AllFailedTransactionErrors = ( TransactionDeclined, InvalidCardNumber, InvalidCardExpirationDate, 54 | InvalidCardCode, InvalidRoutingNumber, InvalidAccountNumber, InvalidBillingAddress, 55 | InvalidBillingZipcode, TransactionAmountLimitExceeded, TransactionFailed, DuplicateTransaction ) 56 | -------------------------------------------------------------------------------- /payment_processor/gateways/__init__.py: -------------------------------------------------------------------------------- 1 | import types, urllib2, urllib 2 | from payment_processor.exceptions import * 3 | from payment_processor import Transaction 4 | from functools import partial 5 | 6 | class GenericGateway( object ): 7 | 8 | transaction_amount_limit = None 9 | 10 | url = None 11 | api = {} 12 | 13 | def __new__( cls, *args, **kwargs ): 14 | if not hasattr( cls, '__clsinit__' ): 15 | cls.__clsinit__ = True 16 | cls.process = cls.checkAmountLimit( cls.process ) 17 | cls.capture = cls.checkAmountLimit( cls.capture ) 18 | return object.__new__(cls) 19 | 20 | def __init__( self, transaction_amount_limit=None ): 21 | if transaction_amount_limit != None: 22 | if transaction_amount_limit <= 0: 23 | raise GatewayInitializeError 24 | self.transaction_amount_limit = transaction_amount_limit 25 | 26 | self.api = self.newAPI() # creates a new api instance from the global api object 27 | 28 | def call( self, transaction, api ): 29 | post_str = '&'.join( [ k + '=' + urllib.quote( str(v) ) for k, v in api.items() if v != None ] ) 30 | 31 | request = urllib2.Request( self.url, post_str ) 32 | 33 | response = urllib2.urlopen( request ).read() 34 | return self.onResponseReady( transaction, response ) 35 | 36 | def onResponseReady( self, transaction, response ): 37 | transaction.last_response = response 38 | self.handleResponse( transaction ) 39 | 40 | def newAPI( self, api=None ): 41 | new_api = dict( self.api ) 42 | if api: new_api.update( api ) 43 | return new_api 44 | 45 | @staticmethod 46 | def checkTransactionStatus( method ): 47 | def transactionStatusChecker( self, transaction, *args, **kwargs ): 48 | 49 | if method.__name__ == 'process': 50 | if transaction.status != Transaction.PENDING: 51 | raise ValueError( "process() requires a transaction with a status of '%s', not '%s'." 52 | % ( Transaction.PENDING, transaction.status ) ) 53 | 54 | if method.__name__ == 'authorize': 55 | if transaction.status != Transaction.PENDING: 56 | raise ValueError( "authorize() requires a transaction with a status of '%s', not '%s'." 57 | % ( Transaction.PENDING, transaction.status ) ) 58 | 59 | if method.__name__ == 'capture': 60 | if transaction.status != Transaction.AUTHORIZED: 61 | raise ValueError( "capture() requires a transaction with a status of '%s', not '%s'." 62 | % ( Transaction.AUTHORIZED, transaction.status ) ) 63 | 64 | if method.__name__ == 'void': 65 | if transaction.status != Transaction.AUTHORIZED: 66 | raise ValueError( "void() requires a transaction with a status of '%s', not '%s'." 67 | % ( Transaction.AUTHORIZED, transaction.status ) ) 68 | 69 | if method.__name__ == 'refund': 70 | if transaction.status != Transaction.CAPTURED: 71 | raise ValueError( "refund() requires a transaction with a status of '%s', not '%s'." 72 | % ( Transaction.CAPTURED, transaction.status ) ) 73 | 74 | if method.__name__ == 'update': 75 | if transaction.status != Transaction.AUTHORIZED: 76 | raise ValueError( "update() requires a transaction with a status of '%s', not '%s'." 77 | % ( Transaction.AUTHORIZED, transaction.status ) ) 78 | 79 | return method( self, transaction, *args, **kwargs ) 80 | return transactionStatusChecker 81 | 82 | @staticmethod 83 | def checkAmountLimit( method ): 84 | def amountLimitChecker( self, transaction, *args, **kwargs ): 85 | if self.transaction_amount_limit != None and transaction.payment.amount > self.transaction_amount_limit: 86 | raise TransactionAmountLimitExceeded( 87 | "The transaction amount of '%d' excedes the gateway's limit of '%d' per transaction" % 88 | ( transaction.payment.amount, self.transaction_amount_limit ) ) 89 | 90 | return method( self, transaction, *args, **kwargs ) 91 | return amountLimitChecker 92 | 93 | def handleResponse( self, transaction ): 94 | raise NotImplementedError 95 | 96 | # Authorize and capture a sale 97 | def process( self, transaction, api=None ): 98 | raise NotImplementedError 99 | 100 | # Authorize a sale 101 | def authorize( self, transaction, api=None ): 102 | raise NotImplementedError 103 | 104 | # Captures funds from a successful authorization 105 | def capture( self, transaction, api=None ): 106 | raise NotImplementedError 107 | 108 | # Void a sale 109 | def void( self, transaction, api=None ): 110 | raise NotImplementedError 111 | 112 | # Refund a processed transaction 113 | def refund( self, transaction, api=None ): 114 | raise NotImplementedError 115 | 116 | # Credits an account 117 | def credit( self, transaction, api=None ): 118 | raise NotImplementedError 119 | 120 | # Updates the order information for the given transaction 121 | def update( self, transaction, api=None ): 122 | raise NotImplementedError 123 | -------------------------------------------------------------------------------- /payment_processor/gateways/authorizenet.py: -------------------------------------------------------------------------------- 1 | # Authorize.net gateways 2 | 3 | from payment_processor.gateways import GenericGateway 4 | from payment_processor.exceptions import * 5 | import payment_processor.methods 6 | 7 | URL_STANDARD = 'https://secure.authorize.net/gateway/transact.dll' 8 | URL_TEST = 'https://test.authorize.net/gateway/transact.dll' 9 | 10 | class AuthorizeNet(): 11 | 12 | gateway = None 13 | 14 | def __init__( self, type='AIM', version='3.1', **kwargs ): 15 | 16 | if type == 'AIM' and version == '3.1': 17 | self.gateway = AuthorizeNetAIM_3_1( **kwargs ) 18 | else: 19 | raise NoGatewayError( 20 | "There is no authorize.net gateway with type '%s' and version '%s'." % ( type, version ) ) 21 | 22 | def __getattr__( self, value ): 23 | return getattr( self.__dict__['gateway'], value ) 24 | 25 | 26 | class AuthorizeNetAIM_3_1( GenericGateway ): 27 | batch_support = False 28 | 29 | url = URL_STANDARD 30 | 31 | api = { 32 | ## Global ## 33 | 'x_delim_data': 'TRUE', 34 | 'x_duplicate_window': '10', 35 | 'x_delim_char': '|', 36 | 'x_relay_response': 'FALSE', 37 | 'x_version': '3.1', 38 | 39 | ## Instance Specific ## 40 | 'x_login': None, 41 | 'x_tran_key': None, 42 | 'x_test_request': 'FALSE', 43 | 'x_allow_partial_Auth': None, 44 | 'x_duplicate_window': None, # Time limit duplicates can not be submitted: between 0 and 28800 45 | 46 | ## Transaction Specific ## 47 | 'x_type': None, # AUTH_CAPTURE (default), AUTH_ONLY, CAPTURE_ONLY, CREDIT, PRIOR_AUTH_CAPTURE, VOID 48 | 'x_method': None, # CC or ECHECK 49 | 'x_amount': None, 50 | 'x_recurring_billing': None, # TRUE, FALSE,T, F, YES, NO, Y, N, 1, 0 51 | 'x_trans_id': None, 52 | 'x_split_tender_id': None, # The payment gateway-assitned ID assigned when the original transaction includes two or more partial payments. 53 | 'x_auth_code': None, # The authorization code of an original transaction not authorized on the payment gateway 54 | 55 | ## CC Specific ## 56 | 'x_card_num': None, 57 | 'x_exp_date': None, # MMYY, MM/YY, MM-YY, MMYYYY, MM/YYYY, MM-YYYY 58 | 'x_card_code': None, 59 | 'x_authentication_indicator': None, 60 | 'x_cardholder_authentication_value': None, 61 | 62 | ## ECHECK Specific ## 63 | 'x_bank_aba_code': None, 64 | 'x_bank_acct_num': None, 65 | 'x_bank_name': None, 66 | 'x_bank_acct_name': None, # CHECKING, BUSINESSCHECKING, SAVINGS 67 | 'x_echeck_type': None, # ARC, BOC, CCD, PPD, TEL, WEB 68 | 'x_bank_check_number': None, 69 | 70 | ## Order Information ## 71 | 'x_invoice_num': None, 72 | 'x_description': None, 73 | 'x_line_item': None, 74 | 'x_po_num': None, 75 | 76 | ## Customer Information ## 77 | 'x_first_name': None, 78 | 'x_last_name': None, 79 | 'x_company': None, 80 | 'x_address': None, 81 | 'x_city': None, 82 | 'x_state': None, 83 | 'x_zip': None, 84 | 'x_country': None, 85 | 'x_phone': None, 86 | 'x_fax': None, 87 | 'x_email': None, 88 | 'x_cust_id': None, 89 | 'x_customer_ip': None, 90 | 91 | ## Shipping Information ## 92 | 'x_ship_to_first_name': None, 93 | 'x_ship_to_last_name': None, 94 | 'x_ship_to_company': None, 95 | 'x_ship_to_address': None, 96 | 'x_ship_to_city': None, 97 | 'x_ship_to_state': None, 98 | 'x_ship_to_zip': None, 99 | 'x_ship_to_country': None, 100 | 'x_tax': None, 101 | 'x_freight': None, 102 | 'x_duty': None, 103 | 'x_tax_exempt': None 104 | 105 | } 106 | 107 | def __init__( self, login=None, trans_key=None, use_test_url=False, enable_test_requests=False, **kwargs ): 108 | GenericGateway.__init__( self, **kwargs ) 109 | 110 | if not login or not trans_key: 111 | raise TypeError( 112 | "The authorize.net gateway requires both a 'login' and 'trans_key' argument." ) 113 | 114 | if use_test_url: 115 | self.url = URL_TEST 116 | 117 | if enable_test_requests: 118 | self.api['x_test_request'] = 'TRUE' 119 | 120 | self.api['x_login'] = login 121 | self.api['x_tran_key'] = trans_key 122 | 123 | @GenericGateway.checkTransactionStatus 124 | def process( self, transaction, api=None ): 125 | api = self.newAPI( api ) 126 | 127 | api['x_type'] = 'AUTH_CAPTURE' 128 | 129 | self.populateAPI( transaction, api ) 130 | 131 | return self.call( transaction, api ) 132 | 133 | @GenericGateway.checkTransactionStatus 134 | def authorize( self, transaction, api=None ): 135 | api = self.newAPI( api ) 136 | 137 | api['x_type'] = 'AUTH_ONLY' 138 | 139 | self.populateAPI( transaction, api ) 140 | 141 | return self.call( transaction, api ) 142 | 143 | @GenericGateway.checkTransactionStatus 144 | def capture( self, transaction, api=None ): 145 | api = self.newAPI( api ) 146 | 147 | #if auth_code != None: 148 | # api['x_type'] = 'CAPTURE_ONLY' 149 | # api['x_auth_code'] = auth_code 150 | 151 | api['x_type'] = 'PRIOR_AUTH_CAPTURE' 152 | api['x_trans_id'] = transaction.trans_id 153 | 154 | return self.call( transaction, api ) 155 | 156 | @GenericGateway.checkTransactionStatus 157 | def void( self, transaction, api=None ): 158 | api = self.newAPI( api ) 159 | 160 | api['x_type'] = 'VOID' 161 | api['x_trans_id'] = transaction.trans_id 162 | 163 | return self.call( transaction, api ) 164 | 165 | @GenericGateway.checkTransactionStatus 166 | def refund( self, transaction, api=None ): 167 | api = self.newAPI( api ) 168 | 169 | api['x_type'] = 'CREDIT' 170 | api['x_trans_id'] = transaction.trans_id 171 | 172 | self.populateAPI( transaction, api ) 173 | 174 | return self.call( transaction, api ) 175 | 176 | def handleResponse( self, transaction ): 177 | response = transaction.last_response.split( self.api['x_delim_char'] ) 178 | print response 179 | ## Response ## 180 | # 0 - Response Code: 1 = Approved, 2 = Declined, 3 = Error, 4 = Held for Review 181 | # 1 - Response Subcode 182 | # 2 - Response Reason Code = http://developer.authorize.net/guides/AIM/Transaction_Response/Response_Reason_Codes_and_Response_Reason_Text.htm 183 | # 3 - Response Reason Text 184 | # 4 - Authorization Code 185 | # 5 - AVS Response 186 | # 6 - Transaction ID 187 | # 7 - Invoice Number 188 | # 8 - Description 189 | # 9 - Amount 190 | # 10 - Method 191 | # 11 - Transaction Type 192 | # 12 - 23 - Customer ID, First Name, Last Name, Company, Address, City, Sate, Zip, Country, Phone, Fax, Email 193 | # 24 - 31 - Ship First Name, Last Name, Company, Address, City, State, Zip, Country 194 | # 32 - Tax 195 | # 33 - Duty 196 | # 34 - Freight 197 | # 35 - Tax Exempt 198 | # 36 - Purchase Order Number 199 | # 37 - MD5 Hash 200 | # 38 - CCV Response 201 | # 39 - CAVV Response 202 | # 40 - Account Number 203 | # 41 - Card Type 204 | # 42 - Split Tender ID 205 | # 43 - Requested Amount 206 | # 44 - Balance on Card 207 | 208 | response_code = int(response[2]) 209 | response_text = response[3] + " (code %s)" % response_code 210 | 211 | transaction.last_response_text = response_text 212 | 213 | if response[6] != '0': 214 | transaction.trans_id = response[6] # transaction id 215 | 216 | ## AVS Response Code Values ## 217 | # A = Address (Street) matches, ZIP does not 218 | # B = Address information not provided for AVS check 219 | # E = AVS errorG = Non-U.S. Card Issuing Bank 220 | # N = No Match on Address (Street) or ZIP 221 | # P = AVS not applicable for this transaction 222 | # R = Retry - System unavailable or timed out 223 | # S = Service not supported by issuer 224 | # U = Address information is unavailable 225 | # W = Nine digit ZIP matches, Address (Street) does not 226 | # X = Address (Street) and nine digit ZIP match 227 | # Y = Address (Street) and five digit ZIP match 228 | # Z = Five digit ZIP matches, Address (Street) does not 229 | avs_response = response[5] 230 | 231 | # M = Match, N = No Match, P = Not Processed, S = Should have been present, U = Issuer unable to process request 232 | ccv_response = response[39] 233 | #print response[0], response[2] 234 | 235 | if response[0] != '1': 236 | 237 | if response_code in ( 6, 37, 200, 315 ): 238 | raise InvalidCardNumber( response_text, response_code=response_code ) 239 | 240 | if response_code in ( 7, 8, 202, 316, 317 ): 241 | raise InvalidCardExpirationDate( response_text, response_code=response_code ) 242 | 243 | if response_code in ( 44, 45, 65 ): 244 | raise InvalidCardCode( response_text, response_code=response_code ) 245 | 246 | if response_code in ( 9, ): 247 | raise InvalidRoutingNumber( response_text, response_code=response_code ) 248 | 249 | if response_code in ( 10, ): 250 | raise InvalidAccountNumber( response_text, response_code=response_code ) 251 | 252 | if response_code in ( 27, 127, 290 ): 253 | if avs_response in ( 'A', ): 254 | raise InvalidBillingZipcode( response_text, response_code=response_code, avs_response=avs_response ) 255 | 256 | raise InvalidBillingAddress( response_text, response_code=response_code, avs_response=avs_response ) 257 | 258 | if response_code in ( 2, 3, 4, 41, 250, 251 ): 259 | raise TransactionDeclined( response_text, response_code=response_code ) 260 | 261 | if response_code in ( 11, 222, 318 ): 262 | raise DuplicateTransaction( response_text, response_code=response_code ) 263 | #print api 264 | # if response[0] == '2': # Declined 265 | # raise ProcessingDeclined( response[3], error_code=response[2], avs_response=avs_response, ccv_response=ccv_response ) 266 | # else: # 3 = Error, 4 = Held for review 267 | 268 | raise TransactionFailed( response_text, response_code=response_code ) 269 | 270 | def populateAPI( self, transaction, api ): 271 | api['x_trans_id'] = transaction.trans_id 272 | api['x_amount'] = transaction.payment.amount 273 | 274 | api['x_first_name'] = transaction.method.first_name 275 | api['x_last_name'] = transaction.method.last_name 276 | api['x_company'] = transaction.method.company 277 | api['x_address'] = transaction.method.address 278 | if transaction.method.address2: 279 | api['x_address']+= ', ' + transaction.method.address2 280 | api['x_city'] = transaction.method.city 281 | api['x_state'] = transaction.method.state 282 | api['x_zip'] = transaction.method.zip_code 283 | api['x_country'] = transaction.method.country or api['x_country'] 284 | api['x_fax'] = transaction.method.fax 285 | api['x_phone'] = transaction.method.phone 286 | api['x_email'] = transaction.method.email 287 | 288 | api['x_customer_ip'] = transaction.payment.ip 289 | api['x_cust_id'] = transaction.payment.customer_id 290 | api['x_invoice_num'] = transaction.payment.order_number 291 | api['x_description'] = transaction.payment.description 292 | 293 | api['x_ship_to_first_name'] = transaction.payment.ship_first_name 294 | api['x_ship_to_last_name'] = transaction.payment.ship_last_name 295 | api['x_ship_to_company'] = transaction.payment.ship_company 296 | api['x_ship_to_address'] = transaction.payment.ship_address 297 | if transaction.payment.ship_address2: 298 | api['x_ship_to_address'] += ', ' + transaction.payment.ship_address2 299 | api['x_ship_to_city'] = transaction.payment.ship_city 300 | api['x_ship_to_state'] = transaction.payment.ship_state 301 | api['x_ship_to_zip'] = transaction.payment.ship_zip_code 302 | api['x_ship_to_country'] = transaction.payment.ship_country 303 | 304 | if transaction.method.__class__ == payment_processor.methods.CreditCard: 305 | 306 | api['x_method'] = 'CC' 307 | api['x_card_num'] = transaction.method.card_number 308 | api['x_exp_date'] = transaction.method.expiration_date.strftime( '%m-%Y' ) 309 | api['x_card_code'] = transaction.method.card_code 310 | 311 | elif transaction.method.__class__ == payment_processor.methods.Check: 312 | 313 | api['x_bank_aba_code'] = transaction.method.routing_number 314 | api['x_bank_acct_num'] = transaction.method.account_number 315 | 316 | api['x_bank_name'] = transaction.method.company \ 317 | or ( transaction.method.first_name or '' ) \ 318 | + ( ' ' + transaction.method.last_name if transaction.method.last_name else '' ) 319 | 320 | api['x_bank_acct_name'] = 'CHECKING' if transaction.method.account_type == payment_processor.methods.Check.CHECKING else 'SAVINGS' 321 | if transaction.method.account_holder_type == payment_processor.methods.Check.BUSINESS: 322 | api['x_bank_acct_name'] = 'BUSINESS' + api['x_bank_acct_name'] 323 | 324 | api['x_echeck_type'] = 'WEB' 325 | api['x_bank_check_number'] = transaction.method.check_number 326 | 327 | else: 328 | raise PaymentMethodUnsupportedByGateway( 329 | "Payment Method '%s' is unsupported by authorize.net AIM 3.1 gateway." % transaction.method.__class__.__name__ ) 330 | -------------------------------------------------------------------------------- /payment_processor/gateways/dummygateway.py: -------------------------------------------------------------------------------- 1 | from payment_processor.gateways import GenericGateway 2 | 3 | class DummyGateway( GenericGateway ): 4 | 5 | def handleResponse( self, transaction ): 6 | pass 7 | 8 | @GenericGateway.checkTransactionStatus 9 | def process( self, transaction, api=None ): 10 | pass 11 | 12 | @GenericGateway.checkTransactionStatus 13 | def authorize( self, transaction, api=None ): 14 | pass 15 | 16 | @GenericGateway.checkTransactionStatus 17 | def capture( self, transaction, api=None ): 18 | pass 19 | 20 | @GenericGateway.checkTransactionStatus 21 | def void( self, transaction, api=None ): 22 | pass 23 | 24 | @GenericGateway.checkTransactionStatus 25 | def refund( self, transaction, api=None ): 26 | pass 27 | 28 | @GenericGateway.checkTransactionStatus 29 | def credit( self, transaction, api=None ): 30 | pass 31 | 32 | @GenericGateway.checkTransactionStatus 33 | def update( self, transaction, api=None ): 34 | pass 35 | -------------------------------------------------------------------------------- /payment_processor/gateways/nationalprocessing.py: -------------------------------------------------------------------------------- 1 | # NationalProcessing gateway 2 | 3 | from payment_processor.gateways import GenericGateway 4 | from payment_processor.exceptions import * 5 | import payment_processor.methods 6 | import urlparse 7 | 8 | class NationalProcessing( GenericGateway ): 9 | 10 | url = 'https://secure.nationalprocessinggateway.com/api/transact.php' 11 | 12 | api = { 13 | ## Global ## 14 | 15 | ## Instance Specific ## 16 | 'username': None, 17 | 'password': None, 18 | 'dup_seconds': None, # Disable duplicates (in seconds) 19 | 20 | ## Transaction Specific ## 21 | 'type': 'auth', # sale / auth / capture / void / refund / credit / update 22 | 'payment': None, # creditcard / check 23 | 'amount': None, 24 | 'sec_code': 'WEB', # PPD / WEB / TEL / CCD 25 | 'processor_id': None, 26 | 'descriptor': None, # Set payment descriptor 27 | 'descriptor_phone': None, # Set payment descriptor phone 28 | 'validation': None, # Specify which Validation processors to use 29 | 30 | ## CC Specific ## 31 | 'ccnumber': None, 32 | 'ccexp': None, # MMYY 33 | 'cvv': None, 34 | 35 | ## Check Specific ## 36 | 'checkname': None, # Name on bank account 37 | 'checkaba': None, 38 | 'checkaccount': None, 39 | 'account_holder_type': None, # business / personal 40 | 'account_type': None, # checking / savings 41 | 42 | ## Order Information ## 43 | 'orderdescription': None, 44 | 'orderid': None, 45 | 'ponumber': None, # Original purchase order 46 | 'tax': None, # Total tax amount 47 | 'shipping': None, # Total shipping amount 48 | #'product_sku_#': None, # Associate API call with Recurring SKU, replace # with an actual number 49 | 50 | ## Customer Information ## 51 | 'firstname': None, 52 | 'lastname': None, 53 | 'company': None, 54 | 'address1': None, 55 | 'address2': None, 56 | 'city': None, 57 | 'state': None, 58 | 'zip': None, 59 | 'country': 'US', 60 | 'phone': None, 61 | 'fax': None, 62 | 'email': None, 63 | 'ipaddress': None, 64 | 65 | ## Shipping Information ## 66 | 'shipping_firstname': None, 67 | 'shipping_lastname': None, 68 | 'shipping_company': None, 69 | 'shipping_address1': None, 70 | 'shipping_address2': None, 71 | 'shipping_city': None, 72 | 'shipping_state': None, 73 | 'shipping_zip': None, 74 | 'shipping_country': 'US', 75 | 'shipping_email': None 76 | } 77 | 78 | def __init__( self, username=None, password=None, **kwargs ): 79 | GenericGateway.__init__( self, **kwargs ) 80 | 81 | if not username or not password: 82 | raise TypeError( 83 | "The National Processing gateway requires both a 'username' and 'password' argument." ) 84 | 85 | self.api['username'] = username 86 | self.api['password'] = password 87 | 88 | @GenericGateway.checkTransactionStatus 89 | def process( self, transaction, api=None ): 90 | api = self.newAPI( api ) 91 | 92 | api['type'] = 'sale' 93 | 94 | self.populateAPI( transaction, api ) 95 | 96 | return self.call( transaction, api ) 97 | 98 | @GenericGateway.checkTransactionStatus 99 | def authorize( self, transaction, api=None ): 100 | api = self.newAPI( api ) 101 | 102 | api['type'] = 'auth' 103 | 104 | self.populateAPI( transaction, api ) 105 | 106 | return self.call( transaction, api ) 107 | 108 | @GenericGateway.checkTransactionStatus 109 | def capture( self, transaction, api=None ): 110 | if not transaction.payment.amount: 111 | raise ValueError( "National Processing's capture() requires a transaction with a defined payment amount." ) 112 | 113 | api = self.newAPI( api ) 114 | 115 | api['type'] = 'capture' 116 | api['transactionid'] = transaction.trans_id 117 | api['amount'] = transaction.payment.amount 118 | api['orderid'] = transaction.payment.order_number 119 | 120 | return self.call( transaction, api ) 121 | 122 | @GenericGateway.checkTransactionStatus 123 | def void( self, transaction, api=None ): 124 | api = self.newAPI( api ) 125 | 126 | api['type'] = 'void' 127 | api['transactionid'] = transaction.trans_id 128 | 129 | return self.call( transaction, api ) 130 | 131 | @GenericGateway.checkTransactionStatus 132 | def refund( self, transaction, api=None ): 133 | api = self.newAPI( api ) 134 | 135 | api['type'] = 'refund' 136 | api['transactionid'] = transaction.trans_id 137 | api['amount'] = transaction.payment.amount 138 | 139 | return self.call( transaction, api ) 140 | 141 | @GenericGateway.checkTransactionStatus 142 | def update( self, transaction, api=None ): 143 | api = self.newAPI( api ) 144 | 145 | api['type'] = 'update' 146 | api['transactionid'] = transaction.trans_id 147 | api['orderid'] = transaction.payment.order_number 148 | 149 | return self.call( transaction, api ) 150 | 151 | def handleResponse( self, transaction ): 152 | 153 | response = urlparse.parse_qs( transaction.last_response ) 154 | 155 | response_code = int( response['response_code'][0] ) 156 | response_text = response['responsetext'][0] + " (code %s)" % response_code 157 | 158 | print response 159 | if 'transactionid' in response: 160 | transaction.trans_id = response['transactionid'][0] 161 | 162 | transaction.last_response_text = response_text 163 | 164 | if response['response'][0] != '1': 165 | 166 | if response_code in ( 221, 222 ) or response_text.startswith('Invalid Credit Card Number'): 167 | raise InvalidCardNumber( response_text, response_code=response_code ) 168 | 169 | if response_code in ( 223, 224 ): 170 | raise InvalidCardExpirationDate( response_text, response_code=response_code ) 171 | 172 | if response_code in ( 225, ): 173 | raise InvalidCardCode( response_text, response_code=response_code ) 174 | 175 | if response_text.startswith('Invalid ABA number'): 176 | raise InvalidRoutingNumber( response_text, response_code=response_code ) 177 | 178 | if response_code in ( 0, ): 179 | raise InvalidAccountNumber( response_text, response_code=response_code ) 180 | 181 | if 'avsresponse' in response and response['avsresponse'][0] in ( 'A', 'B', 'W', 'Z', 'P', 'L', 'N' ): 182 | if avs_response in ( 'A', ): 183 | raise InvalidBillingZipcode( response_text, response_code=response_code, avs_response=avs_response ) 184 | 185 | raise InvalidBillingAddress( response_text, response_code=response_code, avs_response=avs_response ) 186 | 187 | if response_code in ( 240, 250, 251, 252, 253, 260, 261, 262, 263, 264 ): 188 | raise TransactionDeclined( response_text, response_code=response_code ) 189 | #print api 190 | # if response[0] == '2': # Declined 191 | # raise ProcessingDeclined( response[3], error_code=response[2], avs_response=avs_response, ccv_response=ccv_response ) 192 | # else: # 3 = Error, 4 = Held for review 193 | 194 | raise TransactionFailed( response_text, response_code=response_code ) 195 | 196 | def populateAPI( self, transaction, api ): 197 | api['amount'] = transaction.payment.amount 198 | api['firstname'] = transaction.method.first_name 199 | api['lastname'] = transaction.method.last_name 200 | api['company'] = transaction.method.company 201 | api['address1'] = transaction.method.address 202 | api['address2'] = transaction.method.address2 203 | api['city'] = transaction.method.city 204 | api['state'] = transaction.method.state 205 | api['zip'] = transaction.method.zip_code 206 | api['phone'] = transaction.method.phone 207 | api['email'] = transaction.method.email 208 | 209 | api['orderdescription'] = transaction.payment.description 210 | api['orderid'] = transaction.payment.order_number 211 | api['ipaddress'] = transaction.payment.ip 212 | api['shipping_firstname'] = transaction.payment.ship_first_name 213 | api['shipping_lastname'] = transaction.payment.ship_last_name 214 | api['shipping_company'] = transaction.payment.ship_company 215 | api['shipping_address1'] = transaction.payment.ship_address 216 | api['shipping_city'] = transaction.payment.ship_city 217 | api['shipping_state'] = transaction.payment.ship_state 218 | api['shipping_zip'] = transaction.payment.ship_zip_code 219 | api['shipping_email'] = transaction.payment.ship_email 220 | 221 | if transaction.method.__class__ == payment_processor.methods.CreditCard: 222 | 223 | api['payment'] = 'creditcard' 224 | api['ccnumber'] = transaction.method.card_number 225 | api['ccexp'] = transaction.method.expiration_date.strftime( '%m-%Y' ) 226 | api['cvv'] = transaction.method.card_code 227 | 228 | elif transaction.method.__class__ == payment_processor.methods.Check: 229 | 230 | api['payment'] = 'check' 231 | api['checkname'] = transaction.method.company \ 232 | or ( transaction.method.first_name or '' ) \ 233 | + ( ' ' + transaction.method.last_name if transaction.method.last_name else '' ) 234 | api['checkaba'] = '%09d' % int( transaction.method.routing_number ) 235 | api['checkaccount'] = transaction.method.account_number 236 | api['account_holder_type'] = transaction.method.account_holder_type 237 | api['account_type'] = transaction.method.account_type 238 | 239 | else: 240 | raise PaymentMethodUnsupportedByGateway( 241 | "Payment Method '%s' is unsupported by authorize.net AIM 3.1 gateway." % transaction.method.__class__.__name__ ) 242 | -------------------------------------------------------------------------------- /payment_processor/methods/__init__.py: -------------------------------------------------------------------------------- 1 | from payment_processor.exceptions import * 2 | import datetime 3 | 4 | class GenericMethod: 5 | 6 | first_name = None 7 | last_name = None 8 | company = None 9 | card_code = None 10 | address = None 11 | address2 = None 12 | city = None 13 | state = None 14 | zip_code = None 15 | country = None 16 | phone = None 17 | fax = None 18 | email = None 19 | 20 | def __init__( self, first_name=None, last_name=None, company=None, card_code=None, 21 | address=None, address2=None, city=None, state=None, zip_code=None, country=None, 22 | phone=None, fax=None, email=None ): 23 | 24 | self.first_name = first_name 25 | self.last_name = last_name 26 | self.company = company 27 | self.card_code = card_code 28 | self.address = address 29 | self.address2 = address2 30 | self.city = city 31 | self.state = state 32 | self.zip_code = zip_code 33 | self.country = country 34 | self.phone = phone 35 | self.fax = fax 36 | self.email = email 37 | 38 | class CreditCard( GenericMethod ): 39 | 40 | # Required 41 | card_number = None 42 | expiration_date = None # datetime.datetime or datetime.date 43 | 44 | # Optional 45 | card_code = None 46 | 47 | def __init__( self, card_number=None, expiration_date=None, card_code=None, **kwargs ): 48 | 49 | GenericMethod.__init__( self, **kwargs ) 50 | 51 | if not card_number or not expiration_date: 52 | raise TypeError( 53 | "Credit Card method requires both a 'card_number' and 'expiration_date' argument." ) 54 | 55 | if not isinstance( expiration_date, ( datetime.datetime, datetime.date ) ): 56 | raise ValueError( 57 | "Credit method require the 'expiration_date' argument to be of type 'datetime.datetime' or 'datetime.date'." ) 58 | 59 | self.card_number = card_number 60 | self.expiration_date = expiration_date 61 | self.card_code = card_code 62 | 63 | class Check( GenericMethod ): 64 | 65 | CHECKING = 'checking' 66 | SAVINGS = 'savings' 67 | PERSONAL = 'personal' 68 | BUSINESS = 'business' 69 | 70 | account_types = ( CHECKING, SAVINGS ) 71 | account_holder_types = ( PERSONAL, BUSINESS ) 72 | 73 | account_number = None 74 | routing_number = None 75 | account_type = None 76 | account_holder_type = None 77 | check_number = None 78 | 79 | def __init__( self, account_number=None, routing_number=None, account_type=CHECKING, account_holder_type=PERSONAL, 80 | check_number = None, check_checkdigit=True, **kwargs ): 81 | 82 | GenericMethod.__init__( self, **kwargs ) 83 | 84 | if not account_number or not routing_number: 85 | raise TypeError( 86 | "Check method requires both an 'account_number' and 'routing_number' argument." ) 87 | 88 | if not ( self.first_name and self.last_name ) and not self.company: 89 | raise TypeError( 90 | "Check method requires either 'first_name' and 'last_name' arguments or 'company' argument." ) 91 | 92 | if check_checkdigit and not self.validCheckdigit( routing_number ): 93 | raise ValueError( 94 | "Invalid routing_number: checkdigit is invalid." ) 95 | 96 | if account_type not in self.account_types: 97 | raise ValueError( 98 | "Invalid account_type: Either '%s' required but '%s' was provided." % ( self.account_types, account_type ) ) 99 | 100 | if account_holder_type not in self.account_holder_types: 101 | raise ValueError( 102 | "Invalid account_holder_type: Either '%s' required but '%s' was provided." % ( self.account_holder_types, account_holder_type ) ) 103 | 104 | self.account_number = account_number 105 | self.routing_number = routing_number 106 | self.account_type = account_type 107 | self.account_holder_type = account_holder_type 108 | 109 | @classmethod 110 | def validCheckdigit( cls, routing_number, routing_number_length=9 ): 111 | '''Validates the routing number's check digit''' 112 | routing_number = str( routing_number ).rjust( routing_number_length, '0' ) 113 | sum_digit = 0 114 | 115 | for i in range( routing_number_length - 1 ): 116 | n = int( routing_number[i:i+1] ) 117 | sum_digit += n * (3,7,1)[i % 3] 118 | 119 | if sum_digit % 10 > 0: 120 | return 10 - ( sum_digit % 10 ) == int( routing_number[-1] ) 121 | else: 122 | return not int( routing_number[-1] ) 123 | 124 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from distutils.core import setup 3 | 4 | setup( 5 | name = 'payment_processor', 6 | version = '0.2.0', 7 | description = 'A simple payment gateway api wrapper', 8 | author = 'Ian Halpern', 9 | author_email = 'ian@ian-halpern.com', 10 | url = 'https://launchpad.net/python-payment', 11 | download_url = 'https://launchpad.net/python-payment/+download', 12 | packages = ( 13 | 'payment_processor', 14 | 'payment_processor.gateways', 15 | 'payment_processor.methods', 16 | 'payment_processor.exceptions', 17 | 'payment_processor.utils' 18 | ) 19 | ) 20 | --------------------------------------------------------------------------------