diff --git a/paython/gateways/__init__.py b/paython/gateways/__init__.py index 002282e..fac22bc 100644 --- a/paython/gateways/__init__.py +++ b/paython/gateways/__init__.py @@ -4,3 +4,4 @@ from plugnpay import PlugnPay from stripe_com import Stripe from samurai_ff import Samurai +from authecheckdotnet import AuthECheckDotNet diff --git a/paython/gateways/authecheckdotnet.py b/paython/gateways/authecheckdotnet.py new file mode 100644 index 0000000..980bff3 --- /dev/null +++ b/paython/gateways/authecheckdotnet.py @@ -0,0 +1,125 @@ +import time +from paython.exceptions import GatewayError +from paython.gateways.authorize_net import AuthorizeNet +import requests +import copy + +class AuthECheckDotNet(AuthorizeNet): + REQUEST_FIELDS=copy.deepcopy(AuthorizeNet.REQUEST_FIELDS) + REQUEST_FIELDS.update({ + 'aba_code':'x_bank_aba_code', + 'acct_num':'x_bank_acct_num', + 'acct_type':'x_bank_acct_type', + 'bank_name':'x_bank_name', + 'acct_name':'x_bank_acct_name', + 'echeck_type':'x_echeck_type', + 'check_num':'x_bank_check_number', + 'recurring_billing':'x_recurring_billing', + }) + + + def __init__(self, username='test', password='testpassword', debug=False, test=False, delim=None): + #Set Required Values + super(AuthECheckDotNet, self).__init__(username=username, password=password, + debug=debug, test=test,delim=delim) + # Update Fields to bubble up to Base Class + super(AuthorizeNet, self).__init__(translations=self.REQUEST_FIELDS, debug=debug) + + + def auth(self, amount, echeck_type=None, bank_account=None, billing_info=None, shipping_info=None, invoice_num=None, duplicate_window=120, customer_ip=None): + """ + Sends Bank and Check details for authorization + """ + #set up transaction + super(AuthECheckDotNet,self).charge_setup() + """ Change Method to Echeck Instead of CC """ + super(AuthECheckDotNet, self).set('x_method', 'ECHECK') + #setting transaction data + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['amount'], amount) + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['trans_type'], 'AUTH_ONLY') + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['duplicate_window'], duplicate_window) + if customer_ip: + super(AuthorizeNet, self).set(self.REQUEST_FIELDS['ip'], customer_ip) + + if not echeck_type: + debug_string = "No Echeck Type Given" + logger.debug(debug_string) + raise MissingDataError('You did not pass an ECheck Type') + else: + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['echeck_type'], echeck_type) + + if invoice_num is not None: + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['invoice_num'], invoice_num) + + # validating or building up request + if not bank_account: + debug_string = "No Account object present. You passed in %s " % (bank_account) + logger.debug(debug_string) + raise MissingDataError('You did not pass an account object into the arc method') + else: + super(AuthECheckDotNet, self).use_echeck(bank_account) + + #Set Conditionally Required Fields + if echeck_type == 'ARC' or echeck_type == 'BOC': + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['check_num'], bank_account.check_num) + elif echeck_type == 'WEB' or echeck_type == 'TEL': + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['recurring_billing'], bank_account.recurring_billing) + + if billing_info: + super(AuthECheckDotNet, self).set_billing_info(**billing_info) + + if shipping_info: + super(AuthECheckDotNet, self).set_shipping_info(**shipping_info) + + # send transaction to gateway! + response, response_time = super(AuthECheckDotNet,self).request() + return super(AuthECheckDotNet,self).parse(response, response_time) + + def capture(self, amount, echeck_type=None, bank_account=None, billing_info=None, shipping_info=None, invoice_num=None, duplicate_window=120): + """ + Sends Bank and Check details for authorization + """ + #set up transaction + super(AuthECheckDotNet,self).charge_setup() + """ Change Method to Echeck Instead of CC """ + super(AuthECheckDotNet, self).set('x_method', 'ECHECK') + #setting transaction data + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['amount'], amount) + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['trans_type'], 'AUTH_CAPTURE') + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['duplicate_window'], duplicate_window) + + if not echeck_type: + debug_string = "No Echeck Type Given" + logger.debug(debug_string) + raise MissingDataError('You did not pass an ECheck Type') + else: + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['echeck_type'], echeck_type) + + if invoice_num is not None: + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['invoice_num'], invoice_num) + + # validating or building up request + if not bank_account: + debug_string = "No Account object present. You passed in %s " % (bank_account) + logger.debug(debug_string) + raise MissingDataError('You did not pass an account object into the arc method') + else: + super(AuthECheckDotNet, self).use_echeck(bank_account) + + #Set Conditionally Required Fields + if echeck_type == 'ARC' or echeck_type == 'BOC': + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['check_num'], bank_account.check_num) + elif echeck_type == 'WEB' or echeck_type == 'TEL': + super(AuthECheckDotNet, self).set(self.REQUEST_FIELDS['recurring_billing'], bank_account.recurring_billing) + + if billing_info: + super(AuthECheckDotNet, self).set_billing_info(**billing_info) + + if shipping_info: + super(AuthECheckDotNet, self).set_shipping_info(**shipping_info) + + # send transaction to gateway! + response, response_time = super(AuthECheckDotNet,self).request() + return super(AuthECheckDotNet,self).parse(response, response_time) + + diff --git a/paython/gateways/authorize_net.py b/paython/gateways/authorize_net.py index 696da7b..f93c81d 100644 --- a/paython/gateways/authorize_net.py +++ b/paython/gateways/authorize_net.py @@ -2,11 +2,11 @@ import logging from paython.exceptions import MissingDataError -from paython.lib.api import GetGateway +from paython.lib.api import PostGateway logger = logging.getLogger(__name__) -class AuthorizeNet(GetGateway): +class AuthorizeNet(PostGateway): """TODO needs docstring""" VERSION = '3.1' DELIMITER = ';' @@ -60,6 +60,7 @@ class AuthorizeNet(GetGateway): 'alt_trans_id': None, 'split_tender_id':'x_split_tender_id', 'is_partial':'x_allow_partial_auth', + 'duplicate_window': 'x_duplicate_window' } # Response Code: 1 = Approved, 2 = Declined, 3 = Error, 4 = Held for Review @@ -168,19 +169,23 @@ def charge_setup(self): super(AuthorizeNet, self).set('x_delim_data', 'TRUE') super(AuthorizeNet, self).set('x_delim_char', self.DELIMITER) super(AuthorizeNet, self).set('x_version', self.VERSION) + super(AuthorizeNet, self).set('x_method', 'CC') debug_string = " paython.gateways.authorize_net.charge_setup() Just set up for a charge " logger.debug(debug_string.center(80, '=')) - def auth(self, amount, credit_card=None, billing_info=None, shipping_info=None, is_partial=False, split_id=None, invoice_num=None): + def auth(self, amount, credit_card=None, billing_info=None, shipping_info=None, is_partial=False, split_id=None, invoice_num=None, duplicate_window=120, customer_ip=None): """ Sends charge for authorization based on amount """ #set up transaction self.charge_setup() # considering turning this into a decorator? - #setting transaction data super(AuthorizeNet, self).set(self.REQUEST_FIELDS['amount'], amount) super(AuthorizeNet, self).set(self.REQUEST_FIELDS['trans_type'], 'AUTH_ONLY') + super(AuthorizeNet, self).set(self.REQUEST_FIELDS['duplicate_window'], duplicate_window) + if customer_ip: + super(AuthorizeNet, self).set(self.REQUEST_FIELDS['ip'], customer_ip) + if invoice_num is not None: super(AuthorizeNet, self).set(self.REQUEST_FIELDS['invoice_num'], invoice_num) @@ -208,7 +213,7 @@ def auth(self, amount, credit_card=None, billing_info=None, shipping_info=None, response, response_time = self.request() return self.parse(response, response_time) - def settle(self, amount, trans_id, split_id=None): + def settle(self, amount, trans_id, split_id=None, duplicate_window=120): """ Sends prior authorization to be settled based on amount & trans_id PRIOR_AUTH_CAPTURE """ @@ -219,6 +224,7 @@ def settle(self, amount, trans_id, split_id=None): super(AuthorizeNet, self).set(self.REQUEST_FIELDS['trans_type'], 'PRIOR_AUTH_CAPTURE') super(AuthorizeNet, self).set(self.REQUEST_FIELDS['amount'], amount) super(AuthorizeNet, self).set(self.REQUEST_FIELDS['trans_id'], trans_id) + super(AuthorizeNet, self).set(self.REQUEST_FIELDS['duplicate_window'], duplicate_window) if split_id: # settles the entire split super(AuthorizeNet, self).set(self.REQUEST_FIELDS['split_tender_id'], split_id) @@ -312,7 +318,7 @@ def request(self): debug_string = " paython.gateways.authorize_net.request() -- Attempting request to: " logger.debug(debug_string.center(80, '=')) - debug_string = "%s with params: %s" % (url, super(AuthorizeNet, self).query_string()) + debug_string = "%s with params: %s" % (url, super(AuthorizeNet, self).params()) logger.debug(debug_string) logger.debug('as dict: %s' % self.REQUEST_DICT) diff --git a/paython/gateways/core.py b/paython/gateways/core.py index 4978973..a5735b7 100644 --- a/paython/gateways/core.py +++ b/paython/gateways/core.py @@ -32,6 +32,17 @@ def use_credit_card(self, credit_card): self.set(self.REQUEST_FIELDS[key], value) except KeyError: pass # it is okay to fail (on exp_month & exp_year) + + def use_echeck(self, bank_account): + """ + Set up echceck info use (if necessary for transaction) + """ + for key, value in bank_account.__dict__.items(): + if not key.startswith('_'): + try: + self.set(self.REQUEST_FIELDS[key], value) + except KeyError: + pass def set_billing_info(self, address=None, address2=None, city=None, state=None, zipcode=None, country=None, phone=None, email=None, ip=None, first_name=None, last_name=None): """ diff --git a/paython/gateways/stripe_com.py b/paython/gateways/stripe_com.py index e87ade0..c005137 100644 --- a/paython/gateways/stripe_com.py +++ b/paython/gateways/stripe_com.py @@ -37,18 +37,62 @@ def __init__(self, username=None, api_key=None, debug=False): logger.debug(debug_string.center(80, '=')) def auth(self, amount, credit_card=None, billing_info=None, shipping_info=None): - """ - Not implemented because stripe does not support authorizations: - https://answers.stripe.com/questions/can-i-authorize-transactions-first-then-charge-the-customer-after-service-is-comp - """ - raise NotImplementedError('Stripe does not support auth or settlement. Try capture().') + debug_string = " paython.gateways.stripe.parse() -- Sending charge for Authorization" + logger.debug(debug_string.center(80, '=')) + + amount = int(float(amount)*100) # then change the amount to how stripe likes it + + start = time.time() # timing it + try: + response = self.stripe_api.Charge.create( + amount=amount, + currency="usd", + card={ + "name":credit_card.full_name, + "number": credit_card.number, + "exp_month": credit_card.exp_month, + "exp_year": credit_card.exp_year, + "cvc": credit_card.verification_value if credit_card.verification_value else None, + "address_line1":billing_info.get('address'), + "address_line2":billing_info.get('address2'), + "address_zip":billing_info.get('zipcode'), + "address_state":billing_info.get('state'), + }, + capture=False, + ) + except stripe.InvalidRequestError, e: + response = {'failure_message':'Invalid Request: %s' % e} + end = time.time() # done timing it + response_time = '%0.2f' % (end-start) + except stripe.CardError, e: + response = {'failure_message':'Card Error: %s' % e} + end = time.time() # done timing it + response_time = '%0.2f' % (end-start) + else: + end = time.time() # done timing it + response_time = '%0.2f' % (end-start) + + return self.parse(response, response_time) def settle(self, amount, trans_id): - """ - Not implemented because stripe does not support auth/settle: - https://answers.stripe.com/questions/can-i-authorize-transactions-first-then-charge-the-customer-after-service-is-comp - """ - raise NotImplementedError('Stripe does not support auth or settlement. Try capture().') + debug_string = " paython.gateways.stripe.parse() -- Sending charge For Capture with Prior Authorization" + logger.debug(debug_string.center(80, '=')) + + amount = int(float(amount)*100) # then change the amount to how stripe likes it + + start = time.time() # timing it + try: + charge = self.stripe_api.Charge.retrieve(trans_id) + response = charge.capture() + except stripe.InvalidRequestError, e: + response = {'failure_message':'Invalid Request: %s' % e} + end = time.time() # done timing it + response_time = '%0.2f' % (end-start) + else: + end = time.time() # done timing it + response_time = '%0.2f' % (end-start) + + return self.parse(response, response_time) def capture(self, amount, credit_card=None, billing_info=None, shipping_info=None): debug_string = " paython.gateways.stripe.parse() -- Sending charge " diff --git a/paython/lib/__init__.py b/paython/lib/__init__.py index a9dbeeb..46836aa 100644 --- a/paython/lib/__init__.py +++ b/paython/lib/__init__.py @@ -1 +1,2 @@ -from cc import CreditCard \ No newline at end of file +from cc import CreditCard +from echeck import ECheck diff --git a/paython/lib/api.py b/paython/lib/api.py index 75443a7..4795a0e 100644 --- a/paython/lib/api.py +++ b/paython/lib/api.py @@ -171,7 +171,6 @@ def make_request(self, uri): try: params = self.query_string() request = urllib.urlopen('%s%s' % (uri, params)) - return request.read() except: raise GatewayError('Error making request to gateway') diff --git a/paython/lib/echeck.py b/paython/lib/echeck.py new file mode 100644 index 0000000..f15cc43 --- /dev/null +++ b/paython/lib/echeck.py @@ -0,0 +1,61 @@ +from paython.exceptions import DataValidationError +from paython.lib.utils import is_valid_aba + +class ECheck(object): + """ + generic ECheck object + """ + def __init__(self, aba_code, acct_num, acct_type, bank_name,first_name=None, last_name=None, acct_name=None, check_num=None,recurring_billing=None): + """ + sets eCheck info + """ + if acct_name: + self.acct_name = acct_name + else: + self.first_name = first_name + self.last_name = last_name + self.acct_name = "{0.first_name} {0.last_name}".format(self) + + #everything else + self.aba_code = aba_code + self.acct_num = acct_num + self.acct_type = acct_type + self.bank_name = bank_name + self.check_num = check_num + self.recurring_billing = recurring_billing + + def __repr__(self): + """ + string repr for debugging + """ + return u', check_num: {0.check_num}'.format(self) + + + @property + def safe_num(self): + """ + outputs the account number with *'s, only exposing last four digits of account number + """ + account_length = len(self.acct_num) + stars = '*' * (account_length - 4) + return '{0}{1}'.format(stars, self.acct_num[-4:]) + + def is_valid(self): + """ + boolean to see if a Details is valid + """ + try: + self.validate() + except DataValidationError: + return False + else: + return True + + def validate(self): + """ + validates All Codes using util functions + """ + if not is_valid_aba(self.aba_code): + raise DataValidationError('The ABA Code doe not pass validation') + return True diff --git a/paython/lib/utils.py b/paython/lib/utils.py index 6122ba6..a8fee42 100644 --- a/paython/lib/utils.py +++ b/paython/lib/utils.py @@ -120,5 +120,14 @@ def is_valid_email(email): pat = '^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$' return re.search(pat, email, re.IGNORECASE) +def is_valid_aba(aba): + try: + num = map(int, aba) + except ValueError: + return False + else: + return not sum([3*x for x in num[::3]]+[7*x for x in num[1::3]]+[x for x in num[2::3]]) % 10 + def transform_keys(): raise NotImplemented + diff --git a/tests/test_echeck.py b/tests/test_echeck.py new file mode 100644 index 0000000..b6a90b0 --- /dev/null +++ b/tests/test_echeck.py @@ -0,0 +1,60 @@ +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from paython.lib.echeck import ECheck +from paython.exceptions import DataValidationError + +from nose.tools import assert_equals, assert_false, assert_true, with_setup, raises + +# Initialize globals here so that pyflakes doesn't freak out about them. +TEST_ECHECKS = {} +TEST_ABAS = {} + +def setup(): + """setting up the test""" + + global TEST_CARDS + global TEST_ABAS + + TEST_ABAS = [ "123456789123", ] + +def teardown(): + """teardowning the test""" + pass + +@with_setup(setup, teardown) +@raises(DataValidationError) +def test_invalid(): + """test if a ABA Routing Number is invalid""" + echeck = ECheck( + aba_code = "011000014", # invalid ABA CODE + acct_num = "123456789123", + acct_type = "Checking", + bank_name = "bank1", + first_name = "John", + last_name = "Doe", + ) + + # safe check for validity + assert_false(echeck.is_valid()) + + # checking if the exception fires + echeck.validate() + +@with_setup(setup, teardown) +def test_valid(): + """test if a ABA routing number is valid""" + for test_echeck_aba in TEST_ABAS: + # create a credit card object + echeck = ECheck( + aba_code = "011000015", + acct_num = "123456789123", + acct_type = "Checking", + bank_name = "bank1", + first_name = "John", + last_name = "Doe", + ) + + # safe check + assert_true(echeck.is_valid()) + diff --git a/tests/test_utils.py b/tests/test_utils.py index 97c9ea7..6ca94f3 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,7 +1,7 @@ from paython.exceptions import GatewayError -from paython.lib.utils import parse_xml, is_valid_email +from paython.lib.utils import parse_xml, is_valid_email, is_valid_aba -from nose.tools import assert_equals, raises +from nose.tools import assert_equals, raises, assert_true, assert_false @raises(GatewayError) def test_parse_xml(): @@ -39,3 +39,8 @@ def test_append_to_root(): def test_valid_email(): """testing our email validation""" assert_equals(is_valid_email("lol@lol.com") is None, False) + +def test_valid_email(): + """testing ABA number validation""" + assert_true(is_valid_aba("789456124")) + assert_false(is_valid_aba("789456120"))