[Checkins] SVN: zc.authorizedotnet/trunk/src/ fill in project
Benji York
benji at zope.com
Mon May 22 10:19:07 EDT 2006
Log message for revision 68239:
fill in project
Changed:
A zc.authorizedotnet/trunk/src/
A zc.authorizedotnet/trunk/src/zc/
A zc.authorizedotnet/trunk/src/zc/authorizedotnet/
A zc.authorizedotnet/trunk/src/zc/authorizedotnet/README.txt
A zc.authorizedotnet/trunk/src/zc/authorizedotnet/__init__.py
A zc.authorizedotnet/trunk/src/zc/authorizedotnet/processing.py
A zc.authorizedotnet/trunk/src/zc/authorizedotnet/tests.py
-=-
Added: zc.authorizedotnet/trunk/src/zc/authorizedotnet/README.txt
===================================================================
--- zc.authorizedotnet/trunk/src/zc/authorizedotnet/README.txt 2006-05-22 14:04:56 UTC (rev 68238)
+++ zc.authorizedotnet/trunk/src/zc/authorizedotnet/README.txt 2006-05-22 14:19:03 UTC (rev 68239)
@@ -0,0 +1,238 @@
+Authorize.Net Integration
+=========================
+
+Authorize.Net provides credit card (henceforth "CC") processing via a protocol
+on top of HTTPS. Authorize.Net's customers are "merchants". The merchant is
+the entity accepting a CC as payment. This package provides a simple
+interface to Authorize.Net's "Advanced Integration Method" (AIM).
+
+Several terms used in this document:
+
+ - authorize: check validity of CC information and for sufficient balance
+ - capture: the approval of transfer of funds from the CC holder to the
+ merchant
+ - settlement: the actual transfer of funds from the CC holder
+ to the merchant
+ - credit: issuing a refund from the merchant to the card holder
+ - voiding: canceling a previous transaction
+
+Settlement is performed in daily batches. The cut-off time for which is
+specified in the merchant's settings avialable on the Authorize.Net merchant
+interface.
+
+There are many other settings which can be configured via the merchant
+interface, but this module attempts to work independently of most of them.
+Where specific settings are required they will be marked with the phrase
+"required merchant interface setting".
+
+Warning
+-------
+
+This package has NOT been tested in production. It is made available as a
+foundation for future work. We consider it "alpha" quality.
+
+
+Transaction Keys
+----------------
+
+Each AIM transaction must be accomponied by a merchant login and a
+"transaction key". This key is obtained from the merchant interface. After
+importing the CcProcessor class you must pass it your login and transaction
+key:
+
+ >>> from zc.authorizedotnet.processing import CcProcessor
+ >>> cc = CcProcessor(server=SERVER_NAME, login=LOGIN, key=KEY)
+
+
+Authorizing
+-----------
+
+To authorize a charge use the ``authorize`` method. It returns a
+``Transaction`` object.
+
+ >>> result = cc.authorize(amount='1.00', card_num='4007000000027',
+ ... exp_date='0530')
+
+The result object contains details about the transaction.
+
+ >>> result.response
+ 'approved'
+ >>> result.response_reason
+ 'This transaction has been approved.'
+ >>> result.approval_code
+ '123456'
+ >>> result.trans_id
+ '123456789'
+
+The server succesfully processed the transaction:
+
+ >>> transactions = server.getTransactions()
+ >>> transactions[result.trans_id]
+ 'Authorized/Pending Capture'
+
+
+Capturing Authorized Transactions
+---------------------------------
+
+Now if we want to capture the transaction that was previously authorized, we
+can do so.
+
+ >>> result = cc.captureAuthorized(trans_id=result.trans_id)
+ >>> result.response
+ 'approved'
+
+The server shows that the transaction has been captured and is awaiting
+setttlement:
+
+ >>> transactions = server.getTransactions()
+ >>> transactions[result.trans_id]
+ 'Captured/Pending Settlement'
+
+
+Voiding Transactions
+--------------------
+
+If we need to stop a transaction that has not yet been completed (like the
+capturing of the authorized transaction above) we can do so with the ``void``
+method.
+
+ >>> result = cc.void(trans_id=result.trans_id)
+ >>> result.response
+ 'approved'
+
+The server shows that the transaction was voided:
+
+ >>> transactions = server.getTransactions()
+ >>> transactions[result.trans_id]
+ 'Voided'
+
+
+Transaction Errors
+------------------
+
+If something about the transaction is erronious, the transaction results
+indicate so.
+
+ >>> result = cc.authorize(amount='2.00', card_num='4007000000027',
+ ... exp_date='0599')
+
+The result object reflecs the error.
+
+ >>> result.response
+ 'error'
+ >>> result.response_reason
+ 'The credit card has expired.'
+
+The valid values for the ``response`` attribute are 'approved', 'declined',
+and 'error'.
+
+
+Address Verification System (AVS)
+---------------------------------
+
+AVS is used to assert that the billing information provided for a transaction
+must match (to some degree or another) the cardholder's actual billing data.
+The gateway can be configured to disallow transactions that don't meet certain
+AVS critieria.
+
+
+ >>> result = cc.authorize(amount='27.00', card_num='4222222222222',
+ ... exp_date='0530', address='000 Bad Street',
+ ... zip='90210')
+ >>> result.response
+ 'declined'
+ >>> result.response_reason
+ 'The transaction resulted in an AVS mismatch...'
+
+
+Duplicate Window
+----------------
+
+The gateway provides a way to detect and reject duplicate transactions within
+a certain time window. Any transaction with the same CC information (card
+number and exproation date) and amount duplicated within the window will be
+rejected.
+
+The first transaction will work.
+
+ >>> result = cc.authorize(amount='3.00', card_num='4007000000027',
+ ... exp_date='0530')
+ >>> result.response
+ 'approved'
+
+A duplicate transaction will fail with an appropriate message.
+
+ >>> result2 = cc.authorize(amount='3.00', card_num='4007000000027',
+ ... exp_date='0530')
+ >>> result2.response
+ 'error'
+ >>> result2.response_reason
+ 'A duplicate transaction has been submitted.'
+
+The default window size is 120 seconds, but any other value (including 0) can
+be provided by passing ``duplicate_window`` to the transaction method.
+
+ >>> cc.captureAuthorized(trans_id=result.trans_id).response
+ 'approved'
+
+ >>> cc.captureAuthorized(trans_id=result.trans_id).response_reason
+ 'This transaction has already been captured.'
+
+ >>> cc.captureAuthorized(trans_id=result.trans_id, duplicate_window=0
+ ... ).response
+ 'approved'
+
+But voiding doesn't report errors if the same transaction is voided inside
+the duplicate window.
+
+ >>> cc.void(trans_id=result.trans_id).response
+ 'approved'
+
+ >>> cc.void(trans_id=result.trans_id).response
+ 'approved'
+
+The MD5 Hash Security Feature
+-----------------------------
+
+Authorize.Net provides for validating transaction responses via an MD5 hash.
+The required merchant interface setting to use this feature is under
+"Settings and Profile" and then "MD5 Hash". Enter a "salt" value in the
+fields provided and submit the form. You may then provide the ``salt``
+parameter to the CcProcessor constructor to enable response validation.
+
+WARNING: The format of the "amount" field is very important for this feature
+to work correctly. The field must be formatted in the "cononical" way for the
+currency in use. For the US dollar that means no leading zeros and two (and
+only two) decimal places. If the amount is not formatted properly in the
+request the hashes will not match and the transaction will raise an exception.
+
+If you want to enable hash checking, provide a ``salt`` value to the
+``CcProcessor`` constructor. If an incorrect salt value is used, or the
+hash given in the transaction doesn't match the true hash value an exception
+is raised.
+
+ >>> cc = CcProcessor(server='test.authorize.net', login='cnpdev3137',
+ ... key='BLRGz8xUKRafHh1A', salt='wrong')
+ >>> result = cc.authorize(amount='10.00', card_num='4007000000027',
+ ... exp_date='0530')
+ Traceback (most recent call last):
+ ...
+ ValueError: MD5 hash is not valid (trans_id = ...)
+
+
+Error Checking
+--------------
+
+If you don't pass a string for the amount when doing an authorization, an
+exception will be raised. This is to avoid charging the wrong amount due to
+floating point representation issues.
+
+ >>> cc.authorize(amount=5.00, number='4007000000027', expiration='0530')
+ Traceback (most recent call last):
+ ...
+ ValueError: amount must be a string
+
+TODO
+----
+
+ - explain and demonstrate duplicate transaction window
Property changes on: zc.authorizedotnet/trunk/src/zc/authorizedotnet/README.txt
___________________________________________________________________
Name: svn:eol-style
+ native
Added: zc.authorizedotnet/trunk/src/zc/authorizedotnet/__init__.py
===================================================================
--- zc.authorizedotnet/trunk/src/zc/authorizedotnet/__init__.py 2006-05-22 14:04:56 UTC (rev 68238)
+++ zc.authorizedotnet/trunk/src/zc/authorizedotnet/__init__.py 2006-05-22 14:19:03 UTC (rev 68239)
@@ -0,0 +1 @@
+#
Property changes on: zc.authorizedotnet/trunk/src/zc/authorizedotnet/__init__.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.authorizedotnet/trunk/src/zc/authorizedotnet/processing.py
===================================================================
--- zc.authorizedotnet/trunk/src/zc/authorizedotnet/processing.py 2006-05-22 14:04:56 UTC (rev 68238)
+++ zc.authorizedotnet/trunk/src/zc/authorizedotnet/processing.py 2006-05-22 14:19:03 UTC (rev 68239)
@@ -0,0 +1,103 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from M2Crypto import httpslib
+import urllib
+import md5
+
+class TransactionResult(object):
+ def __init__(self, fields):
+ response_code_map = {'1': 'approved', '2': 'declined', '3': 'error'}
+ self.response = response_code_map[fields[0]]
+ self.response_reason = fields[3]
+ TESTING_PREFIX = '(TESTMODE) '
+ if self.response_reason.startswith(TESTING_PREFIX):
+ self.response_reason = self.response_reason[len(TESTING_PREFIX):]
+ self.approval_code = fields[4]
+ self.trans_id = fields[6]
+ self.amount = fields[9]
+ self.hash = fields[37]
+
+ def validateHash(self, login, salt):
+ value = ''.join([salt, login, self.trans_id, self.amount])
+ return self.hash == md5.new(value).hexdigest()
+
+
+class AuthorizeNetConnection(object):
+ def __init__(self, server, login, key, salt=None):
+ self.server = server
+ self.login = login
+ self.salt = salt
+ self.delimiter = '|'
+ self.standard_fields = dict(
+ x_login = login,
+ x_tran_key = key,
+ x_version = '3.1',
+ x_delim_data = 'TRUE',
+ x_delim_char = self.delimiter,
+ x_relay_response = 'FALSE',
+ x_method = 'CC',
+ )
+
+ def sendTransaction(self, **kws):
+ # if the card number passed in is the "generate an error" card...
+ if kws.get('card_num') == '4222222222222':
+ # ... turn on test mode (that's the only time that card works)
+ kws['test_request'] = 'TRUE'
+
+ fields = dict(('x_'+key, value) for key, value in kws.iteritems())
+ fields.update(self.standard_fields)
+ body = urllib.urlencode(fields)
+
+ if self.server.startswith('localhost:'):
+ server, port = self.server.split(':')
+ conn = httpslib.HTTPConnection(server, port)
+ else:
+ conn = httpslib.HTTPSConnection(self.server)
+ conn.putrequest('POST', '/gateway/transact.dll')
+ conn.putheader('content-type', 'application/x-www-form-urlencoded')
+ conn.putheader('content-length', len(body))
+ conn.endheaders()
+ conn.send(body)
+
+ response = conn.getresponse()
+ fields = response.read().split(self.delimiter)
+ result = TransactionResult(fields)
+
+ if (self.salt is not None
+ and not result.validateHash(self.login, self.salt)):
+ raise ValueError('MD5 hash is not valid (trans_id = %r)'
+ % result.trans_id)
+
+ return result
+
+
+class CcProcessor(object):
+ def __init__(self, server, login, key, salt=None):
+ self.connection = AuthorizeNetConnection(server, login, key, salt)
+
+ def authorize(self, **kws):
+ if not isinstance(kws['amount'], basestring):
+ raise ValueError('amount must be a string')
+
+ type = 'AUTH_ONLY'
+ return self.connection.sendTransaction(type=type, **kws)
+
+ def captureAuthorized(self, **kws):
+ type = 'PRIOR_AUTH_CAPTURE'
+ return self.connection.sendTransaction(type=type, **kws)
+
+ def void(self, **kws):
+ type = 'VOID'
+ return self.connection.sendTransaction(type=type, **kws)
Property changes on: zc.authorizedotnet/trunk/src/zc/authorizedotnet/processing.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
Added: zc.authorizedotnet/trunk/src/zc/authorizedotnet/tests.py
===================================================================
--- zc.authorizedotnet/trunk/src/zc/authorizedotnet/tests.py 2006-05-22 14:04:56 UTC (rev 68238)
+++ zc.authorizedotnet/trunk/src/zc/authorizedotnet/tests.py 2006-05-22 14:19:03 UTC (rev 68239)
@@ -0,0 +1,261 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from BeautifulSoup import BeautifulSoup
+from zope.testbrowser.browser import Browser
+from zope.testing import doctest, renormalizing
+import BaseHTTPServer
+import SimpleHTTPServer
+import cgi
+import httplib
+import os
+import re
+import threading
+import unittest
+
+TEST_SERVER_PORT = 30423
+
+class ScrapedMerchantUiServer(object):
+ initialized = False
+
+ def __init__(self, server, login, password):
+ login_page = 'https://%s/ui/themes/anet/merch.app' % server
+ self.browser = Browser()
+ self.browser.open(login_page)
+ self.browser.getControl(name='MerchantLogin').value = login
+ self.browser.getControl(name='Password').value = password
+ self.browser.getControl('Log In').click()
+
+ def getTransactions(self):
+ self.browser.getLink('Unsettled Transactions').click()
+ soup = BeautifulSoup(self.browser.contents)
+ transactions = {}
+ for row in soup('tr', ['TblRowFont', 'TblRowFontWithBGColor']):
+ cells = row('td')
+ if len(cells) == 8:
+ txn_id = cells[0].contents[0].contents[0]
+ txn_status = cells[2].contents[0]
+ transactions[txn_id] = txn_status
+
+ return transactions
+
+ def close(self):
+ self.browser.mech_browser.close()
+
+
+class InProcessServer(object):
+ info = {}
+ last_transaction_id = 0
+ seen_auths = []
+ seen_captures = []
+
+ def getTransactions(self):
+ return self.info
+
+
+class TestRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+
+ def do_POST(self):
+ fields = cgi.FieldStorage(fp=self.rfile, headers=self.headers,
+ environ = {'REQUEST_METHOD':'POST'},
+ keep_blank_values = 1)
+
+ if fields.getvalue('x_delim_data') != 'TRUE':
+ raise ValueError('the test server only knows how to do'
+ ' delimited output')
+
+ if fields.getvalue('x_relay_response') != 'FALSE':
+ raise ValueError('AIM requires x_relay_response be "FALSE"')
+
+ required_fields = ('x_login x_tran_key x_version x_delim_char '
+ 'x_method').split()
+ for f in required_fields:
+ if f not in fields:
+ raise ValueError('the %r field is required' % f)
+
+ delimiter = fields['x_delim_char'].value
+
+ self.send_response(200)
+ self.send_header('Content-Type', 'text/plain')
+ self.end_headers()
+
+ out = getattr(self, fields['x_type'].value)(fields)
+ self.wfile.write(delimiter.join(out))
+
+ @staticmethod
+ def makeOut(response_code, reason_text, approval_code, trans_id, amount):
+ hash = 'DEADBEEF'
+ out = [''] * 38
+ out[:9] = [response_code, '', '', reason_text, approval_code, '',
+ trans_id, '', '', amount]
+ out[37] = hash
+ return out
+
+ def getTransactionId(self):
+ in_process_server.last_transaction_id += 1
+ return '%09d' % in_process_server.last_transaction_id
+
+ def AUTH_ONLY(self, fields):
+ amount = fields.getvalue('x_amount')
+ card_num = fields.getvalue('x_card_num')
+ expr = fields.getvalue('x_exp_date')
+
+ response_code = '1'
+ reason_text = 'This transaction has been approved.'
+ approval_code = '123456'
+ trans_id = self.getTransactionId()
+
+ if expr[2:4] > '80':
+ response_code = '3'
+ reason_text = 'The credit card has expired.'
+
+ if card_num == '4222222222222' and amount.split('.')[0] == '27':
+ response_code = '2'
+ reason_text = ('The transaction resulted in an AVS mismatch. The'
+ ' address provided does not match billing address'
+ ' of cardholder.')
+
+ if (amount, card_num) in in_process_server.seen_auths:
+ response_code = '3'
+ reason_text = 'A duplicate transaction has been submitted.'
+
+ out = self.makeOut(response_code, reason_text, approval_code,
+ trans_id, amount)
+
+ if (amount, card_num) not in in_process_server.seen_auths:
+ in_process_server.seen_auths.append( (amount, card_num) )
+ in_process_server.info[trans_id] = 'Authorized/Pending Capture'
+
+ return out
+
+ def VOID(self, fields):
+ trans_id = fields.getvalue('x_trans_id')
+
+ response_code = '1'
+ reason_text = 'This transaction has been approved.'
+ approval_code = '123456'
+ out = self.makeOut(response_code, reason_text, approval_code,
+ trans_id, amount='')
+ in_process_server.info[trans_id] = 'Voided'
+ return out
+
+ prev_captures = []
+ def PRIOR_AUTH_CAPTURE(self, fields):
+ trans_id = fields.getvalue('x_trans_id')
+ duplicate_window = fields.getvalue('x_duplicate_window')
+
+ response_code = '1'
+ reason_text = 'This transaction has been approved.'
+ approval_code = '123456'
+
+ if (trans_id in in_process_server.seen_captures
+ and duplicate_window != '0'):
+ response_code = '3'
+ reason_text = 'This transaction has already been captured.'
+
+ out = self.makeOut(response_code, reason_text, approval_code,
+ trans_id, amount='')
+
+ if trans_id not in in_process_server.seen_captures:
+ in_process_server.seen_captures.append(trans_id)
+ in_process_server.info[trans_id] = 'Captured/Pending Settlement'
+
+ return out
+
+ def do_QUIT(self):
+ self.send_response(200)
+ self.end_headers()
+ self.server.stop = True
+
+ def log_message(self, format, *args):
+ pass
+
+
+class TestHttpServer(BaseHTTPServer.HTTPServer):
+
+ def serve_forever (self):
+ """Handle one request at a time until stopped."""
+ self.stop = False
+ while not self.stop:
+ self.handle_request()
+
+
+http_server = TestHttpServer(('localhost', TEST_SERVER_PORT),
+ TestRequestHandler)
+in_process_server = InProcessServer()
+
+def localSetUp(test):
+ http_server.thread = threading.Thread(target=http_server.serve_forever)
+ http_server.thread.start()
+ test.globs['LOGIN'] = 'LOGIN'
+ test.globs['KEY'] = 'KEY'
+
+def localTearDown(test):
+ conn = httplib.HTTPConnection('localhost:%d' % TEST_SERVER_PORT)
+ conn.request('QUIT', '/')
+ conn.getresponse()
+ http_server.thread.join()
+
+def remoteSetUp(test):
+ login = os.environ.get('AUTHORIZE_DOT_NET_LOGIN')
+ password = os.environ.get('AUTHORIZE_DOT_NET_PASSWORD')
+ key = os.environ.get('AUTHORIZE_DOT_NET_TRANSACTION_KEY')
+
+ if login is None or password is None or key is None:
+ raise RuntimeError('all of AUTHORIZE_DOT_NET_LOGIN,'
+ ' AUTHORIZE_DOT_NET_PASSWORD, and'
+ ' AUTHORIZE_DOT_NET_TRANSACTION_KEY must be'
+ ' provided in order to run the zc.authorizedotnet'
+ ' tests against the Authorize.Net test server.')
+
+ test.globs['server'] = ScrapedMerchantUiServer('test.authorize.net', login,
+ password)
+ test.globs['LOGIN'] = login
+ test.globs['KEY'] = key
+
+def remoteTearDown(test):
+ test.globs['server'].close()
+
+def test_suite():
+ checker = renormalizing.RENormalizing([
+ (re.compile(r"'\d{6}'"), "'123456'"), # for approval codes
+ (re.compile(r"'\d{9}'"), "'123456789'"), # for transaction IDs
+ ])
+
+ remote = doctest.DocFileSuite(
+ 'README.txt',
+ globs = dict(
+ SERVER_NAME='test.authorize.net',
+ ),
+ optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS,
+ checker = checker,
+ setUp = remoteSetUp,
+ tearDown = remoteTearDown,
+ )
+ remote.level = 5
+ local = doctest.DocFileSuite(
+ 'README.txt',
+ globs = dict(
+ server=in_process_server,
+ SERVER_NAME='localhost:%s' % TEST_SERVER_PORT,
+ ),
+ optionflags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS,
+ checker = checker,
+ setUp = localSetUp,
+ tearDown = localTearDown,
+ )
+ return unittest.TestSuite((remote, local))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
Property changes on: zc.authorizedotnet/trunk/src/zc/authorizedotnet/tests.py
___________________________________________________________________
Name: svn:keywords
+ Id
Name: svn:eol-style
+ native
More information about the Checkins
mailing list