commit
cf4d334b71
|
@ -51,6 +51,7 @@ from regluit.frontend.forms import EbookForm, CustomPremiumForm, EditManagersFor
|
|||
from regluit.frontend.forms import getTransferCreditForm, CCForm, CloneCampaignForm
|
||||
from regluit.payment.manager import PaymentManager
|
||||
from regluit.payment.models import Transaction, Account
|
||||
from regluit.payment import baseprocessor
|
||||
from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_CANCELED, TRANSACTION_STATUS_ERROR, TRANSACTION_STATUS_FAILED, TRANSACTION_STATUS_INCOMPLETE, TRANSACTION_STATUS_NONE, TRANSACTION_STATUS_MODIFIED
|
||||
from regluit.payment.parameters import PAYMENT_TYPE_AUTHORIZATION, PAYMENT_TYPE_INSTANT
|
||||
from regluit.payment.parameters import PAYMENT_HOST_STRIPE, PAYMENT_HOST_NONE
|
||||
|
@ -788,8 +789,8 @@ class FundPledgeView(FormView):
|
|||
try:
|
||||
account = p.make_account(transaction.user, stripe_token, host=transaction.host)
|
||||
logger.info('account.id: {0}'.format(account.id))
|
||||
except Exception, e:
|
||||
raise e
|
||||
except baseprocessor.ProcessorError as e:
|
||||
return HttpResponse("baseprocessor.ProcessorError: {0}".format(e))
|
||||
|
||||
# GOAL: deactivate any older accounts associated with user
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from regluit.payment.models import PaymentResponse
|
||||
from regluit.payment.models import PaymentResponse
|
||||
|
||||
from django.http import HttpResponseForbidden
|
||||
from datetime import timedelta
|
||||
|
@ -7,6 +7,13 @@ from regluit.utils.localdatetime import now, zuluformat
|
|||
import datetime
|
||||
import time
|
||||
|
||||
class ProcessorError(Exception):
|
||||
"""An abstraction around payment processor exceptions"""
|
||||
def __init__(self, message=None, original_exception=None):
|
||||
super(ProcessorError, self).__init__(message)
|
||||
self.original_exception = original_exception
|
||||
|
||||
|
||||
|
||||
class BasePaymentRequest:
|
||||
'''
|
||||
|
|
|
@ -17,7 +17,7 @@ import stripe
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class StripeError(Exception):
|
||||
class StripelibError(baseprocessor.ProcessorError):
|
||||
pass
|
||||
|
||||
try:
|
||||
|
@ -75,6 +75,8 @@ ERROR_TESTING = dict((
|
|||
('CHARGE_DECLINE', ('4000000000000002', 'Charges with this card will always be declined.'))
|
||||
))
|
||||
|
||||
CARD_FIELDS_TO_COMPARE = ('exp_month', 'exp_year', 'name', 'address_line1', 'address_line2', 'address_zip', 'address_state')
|
||||
|
||||
# types of errors / when they can be handled
|
||||
|
||||
#card_declined: Use this special card number - 4000000000000002.
|
||||
|
@ -89,16 +91,36 @@ def filter_none(d):
|
|||
|
||||
# if you create a Customer object, then you'll be able to charge multiple times. You can create a customer with a token.
|
||||
|
||||
# http://en.wikipedia.org/wiki/Luhn_algorithm#Implementation_of_standard_Mod_10
|
||||
|
||||
def luhn_checksum(card_number):
|
||||
def digits_of(n):
|
||||
return [int(d) for d in str(n)]
|
||||
digits = digits_of(card_number)
|
||||
odd_digits = digits[-1::-2]
|
||||
even_digits = digits[-2::-2]
|
||||
checksum = 0
|
||||
checksum += sum(odd_digits)
|
||||
for d in even_digits:
|
||||
checksum += sum(digits_of(d*2))
|
||||
return checksum % 10
|
||||
|
||||
def is_luhn_valid(card_number):
|
||||
return luhn_checksum(card_number) == 0
|
||||
|
||||
|
||||
# https://stripe.com/docs/tutorials/charges
|
||||
|
||||
def card (number=TEST_CARDS[0][0], exp_month='01', exp_year='2020', cvc=None, name=None,
|
||||
def card (number=TEST_CARDS[0][0], exp_month=1, exp_year=2020, cvc=None, name=None,
|
||||
address_line1=None, address_line2=None, address_zip=None, address_state=None, address_country=None):
|
||||
|
||||
"""Note: there is no place to enter address_city in the API"""
|
||||
|
||||
card = {
|
||||
"number": number,
|
||||
"exp_month": str(exp_month),
|
||||
"exp_year": str(exp_year),
|
||||
"cvc": str(cvc) if cvc is not None else None,
|
||||
"exp_month": int(exp_month),
|
||||
"exp_year": int(exp_year),
|
||||
"cvc": int(cvc) if cvc is not None else None,
|
||||
"name": name,
|
||||
"address_line1": address_line1,
|
||||
"address_line2": address_line2,
|
||||
|
@ -140,7 +162,7 @@ class StripeClient(object):
|
|||
def create_token(self, card):
|
||||
return stripe.Token(api_key=self.api_key).create(card=card)
|
||||
|
||||
def create_customer (self, card=None, description=None, email=None, account_balance=None, plan=None, trial_end=None):
|
||||
def create_customer(self, card=None, description=None, email=None, account_balance=None, plan=None, trial_end=None):
|
||||
"""card is a dictionary or a token"""
|
||||
# https://stripe.com/docs/api?lang=python#create_customer
|
||||
|
||||
|
@ -183,7 +205,6 @@ class StripeClient(object):
|
|||
# https://stripe.com/docs/api?lang=python#list_charges
|
||||
return stripe.Charge(api_key=self.api_key).all(count=count, offset=offset, customer=customer)
|
||||
|
||||
# what to work through?
|
||||
|
||||
# can't test Transfer in test mode: "There are no transfers in test mode."
|
||||
|
||||
|
@ -191,10 +212,6 @@ class StripeClient(object):
|
|||
# bad card -- what types of erros to handle?
|
||||
# https://stripe.com/docs/api#errors
|
||||
|
||||
# what errors are handled in the python library and how?
|
||||
#
|
||||
|
||||
# Account?
|
||||
|
||||
# https://stripe.com/docs/api#event_types
|
||||
# events of interest -- especially ones that do not directly arise immediately (synchronously) from something we do -- I think
|
||||
|
@ -212,6 +229,168 @@ class StripeClient(object):
|
|||
# how to tell whether money transferred to bank account yet
|
||||
# best practices for calling Events -- not too often.
|
||||
|
||||
|
||||
# Errors we still need to catch:
|
||||
#
|
||||
# * invalid_number -- can't get stripe to generate for us. What it means:
|
||||
#
|
||||
# * that the card has been cancelled (or never existed to begin with
|
||||
#
|
||||
# * the card is technically correct (Luhn valid?)
|
||||
#
|
||||
# * the first 6 digits point to a valid bank
|
||||
#
|
||||
# * but the account number (the rest of the digits) doesn't correspond to a credit account with that bank
|
||||
#
|
||||
# * Brian of stripe.com suggests we could treat it the same way as we'd treat card_declined
|
||||
#
|
||||
# * processing_error:
|
||||
#
|
||||
# * means: something went wrong when stripe tried to make the charge (it could be that the card's issuing bank is down, or our connection to the bank isn't working properly)
|
||||
# * we can retry -- e.g., a minute later, then 30 minutes, then an hour, 3 hours, a day.
|
||||
# * we shouldn't see processing_error very often
|
||||
#
|
||||
# * expired_card -- also not easily simulatable in test mode
|
||||
|
||||
|
||||
class StripeErrorTest(TestCase):
|
||||
"""Make sure the exceptions returned by stripe act as expected"""
|
||||
|
||||
def test_cc_test_numbers_luhn_valid(self):
|
||||
"""Show that the test CC numbers supplied for testing as valid numbers are indeed Luhn valid"""
|
||||
self.assertTrue(all([is_luhn_valid(c[0]) for c in ERROR_TESTING.values()]))
|
||||
|
||||
def test_good_token(self):
|
||||
""" verify normal operation """
|
||||
sc = StripeClient()
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=1, exp_year='2020', cvc='123', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
|
||||
token1 = sc.create_token(card=card1)
|
||||
# use the token id -- which is what we get from JavaScript api -- and retrieve the token
|
||||
token2 = sc.token.retrieve(id=token1.id)
|
||||
self.assertEqual(token2.id, token1.id)
|
||||
# make sure token id has a form tok_
|
||||
self.assertEqual(token2.id[:4], "tok_")
|
||||
|
||||
# should be only test mode
|
||||
self.assertEqual(token2.livemode, False)
|
||||
# token hasn't been used yet
|
||||
self.assertEqual(token2.used, False)
|
||||
# test that card info matches up with what was fed in.
|
||||
for k in CARD_FIELDS_TO_COMPARE:
|
||||
self.assertEqual(token2.card[k], card1[k])
|
||||
# last4
|
||||
self.assertEqual(token2.card.last4, TEST_CARDS[0][0][-4:])
|
||||
# fingerprint
|
||||
self.assertGreaterEqual(len(token2.card.fingerprint), 16)
|
||||
|
||||
# now charge the token
|
||||
charge1 = sc.create_charge(10, 'usd', card=token2.id)
|
||||
self.assertEqual(charge1.amount, 1000)
|
||||
self.assertEqual(charge1.id[:3], "ch_")
|
||||
# disputed, failure_message, fee, fee_details
|
||||
self.assertEqual(charge1.disputed,False)
|
||||
self.assertEqual(charge1.failure_message,None)
|
||||
self.assertEqual(charge1.fee,59)
|
||||
self.assertEqual(charge1.refunded,False)
|
||||
|
||||
|
||||
def test_error_creating_customer_with_declined_card(self):
|
||||
"""Test whether we can get a charge decline error"""
|
||||
sc = StripeClient()
|
||||
card1 = card(number=ERROR_TESTING['CHARGE_DECLINE'][0])
|
||||
try:
|
||||
cust1 = sc.create_customer(card=card1, description="This card should fail")
|
||||
self.fail("Attempt to create customer did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "card_declined")
|
||||
self.assertEqual(e.message, "Your card was declined.")
|
||||
|
||||
def test_charge_bad_cust(self):
|
||||
# expect the card to be declined -- and for us to get CardError
|
||||
sc = StripeClient()
|
||||
# bad card
|
||||
card1 = card(number=ERROR_TESTING['BAD_ATTACHED_CARD'][0])
|
||||
# attaching card should be ok
|
||||
cust1 = sc.create_customer(card=card1, description="test bad customer", email="rdhyee@gluejar.com")
|
||||
# trying to charge the card should fail
|
||||
self.assertRaises(stripe.CardError, sc.create_charge, 10,
|
||||
customer = cust1.id, description="$10 for bad cust")
|
||||
|
||||
def test_bad_cc_number(self):
|
||||
"""send a bad cc and should get an error when trying to create a token"""
|
||||
BAD_CC_NUM = '4242424242424241'
|
||||
|
||||
# reason for decline is number is not Luhn valid
|
||||
self.assertFalse(is_luhn_valid(BAD_CC_NUM))
|
||||
|
||||
sc = StripeClient()
|
||||
card1 = card(number=BAD_CC_NUM, exp_month=1, exp_year=2020, cvc='123', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
|
||||
|
||||
try:
|
||||
token1 = sc.create_token(card=card1)
|
||||
self.fail("Attempt to create token with bad cc number did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "incorrect_number")
|
||||
self.assertEqual(e.message, "Your card number is incorrect")
|
||||
|
||||
def test_invalid_expiry_month(self):
|
||||
"""Use an invalid month e.g. 13."""
|
||||
|
||||
sc = StripeClient()
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=13, exp_year=2020, cvc='123', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None)
|
||||
|
||||
try:
|
||||
token1 = sc.create_token(card=card1)
|
||||
self.fail("Attempt to create token with invalid expiry month did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "invalid_expiry_month")
|
||||
self.assertEqual(e.message, "Your card's expiration month is invalid")
|
||||
|
||||
def test_invalid_expiry_year(self):
|
||||
"""Use a year in the past e.g. 1970."""
|
||||
|
||||
sc = StripeClient()
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=12, exp_year=1970, cvc='123', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None)
|
||||
|
||||
try:
|
||||
token1 = sc.create_token(card=card1)
|
||||
self.fail("Attempt to create token with invalid expiry year did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "invalid_expiry_year")
|
||||
self.assertEqual(e.message, "Your card's expiration year is invalid")
|
||||
|
||||
def test_invalid_cvc(self):
|
||||
"""Use a two digit number e.g. 99."""
|
||||
|
||||
sc = StripeClient()
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=12, exp_year=2020, cvc='99', name='Don Giovanni',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None)
|
||||
|
||||
try:
|
||||
token1 = sc.create_token(card=card1)
|
||||
self.fail("Attempt to create token with invalid cvc did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "invalid_cvc")
|
||||
self.assertEqual(e.message, "Your card's security code is invalid")
|
||||
|
||||
def test_missing_card(self):
|
||||
"""There is no card on a customer that is being charged"""
|
||||
|
||||
sc = StripeClient()
|
||||
# create a Customer with no attached card
|
||||
cust1 = sc.create_customer(description="test cust w/ no card")
|
||||
try:
|
||||
sc.create_charge(10, customer = cust1.id, description="$10 for cust w/ no card")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "missing")
|
||||
self.assertEqual(e.message, "Cannot charge a customer that has no active card")
|
||||
|
||||
|
||||
|
||||
class PledgeScenarioTest(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
@ -244,7 +423,6 @@ class PledgeScenarioTest(TestCase):
|
|||
self.assertRaises(stripe.CardError, self._sc.create_charge, 10,
|
||||
customer = self._cust_bad_card.id, description="$10 for bad cust")
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# clean up stuff we create in test -- right now list current objects
|
||||
|
@ -273,8 +451,12 @@ class Processor(baseprocessor.Processor):
|
|||
sc = StripeClient()
|
||||
|
||||
# create customer and charge id and then charge the customer
|
||||
customer = sc.create_customer(card=token, description=user.username,
|
||||
try:
|
||||
customer = sc.create_customer(card=token, description=user.username,
|
||||
email=user.email)
|
||||
except stripe.StripeError as e:
|
||||
raise StripelibError(e.message, e)
|
||||
|
||||
|
||||
account = Account(host = PAYMENT_HOST_STRIPE,
|
||||
account_id = customer.id,
|
||||
|
@ -322,7 +504,7 @@ class Processor(baseprocessor.Processor):
|
|||
logger.warning("user {0} has more than one active payment account".format(transaction.user))
|
||||
elif transaction.user.account_set.filter(date_deactivated__isnull=True).count() == 0:
|
||||
logger.warning("user {0} has no active payment account".format(transaction.user))
|
||||
raise StripeError("user {0} has no active payment account".format(transaction.user))
|
||||
raise StripelibError("user {0} has no active payment account".format(transaction.user))
|
||||
|
||||
account = transaction.user.account_set.filter(date_deactivated__isnull=True)[0]
|
||||
logger.info("user: {0} customer.id is {1}".format(transaction.user, account.account_id))
|
||||
|
@ -368,11 +550,18 @@ class Processor(baseprocessor.Processor):
|
|||
# is it a customer or a token?
|
||||
|
||||
# BUGBUG: replace description with somethin more useful
|
||||
# TO DO: rewrapping StripeError to StripelibError -- but maybe we should be more specific
|
||||
if transaction.preapproval_key.startswith('cus_'):
|
||||
charge = sc.create_charge(transaction.amount, customer=transaction.preapproval_key, description="${0} for test / retain cc".format(transaction.amount))
|
||||
try:
|
||||
charge = sc.create_charge(transaction.amount, customer=transaction.preapproval_key, description="${0} for test / retain cc".format(transaction.amount))
|
||||
except stripe.StripeError as e:
|
||||
raise StripelibError(e.message, e)
|
||||
elif transaction.preapproval_key.startswith('tok_'):
|
||||
charge = sc.create_charge(transaction.amount, card=transaction.preapproval_key, description="${0} for test / cc not retained".format(transaction.amount))
|
||||
|
||||
try:
|
||||
charge = sc.create_charge(transaction.amount, card=transaction.preapproval_key, description="${0} for test / cc not retained".format(transaction.amount))
|
||||
except stripe.StripeError as e:
|
||||
raise StripelibError(e.message, e)
|
||||
|
||||
transaction.status = TRANSACTION_STATUS_COMPLETE
|
||||
transaction.pay_key = charge.id
|
||||
transaction.date_payment = now()
|
||||
|
@ -415,8 +604,8 @@ class Processor(baseprocessor.Processor):
|
|||
|
||||
def suite():
|
||||
|
||||
testcases = [PledgeScenarioTest]
|
||||
#testcases = []
|
||||
#testcases = [PledgeScenarioTest, StripeErrorTest]
|
||||
testcases = [StripeErrorTest]
|
||||
suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases])
|
||||
#suites.addTest(LibraryThingTest('test_cache'))
|
||||
#suites.addTest(SettingsTest('test_dev_me_alignment')) # give option to test this alignment
|
||||
|
|
Loading…
Reference in New Issue