|
|
|
@ -12,8 +12,8 @@ from itertools import islice
|
|
|
|
|
from pytz import utc
|
|
|
|
|
import re
|
|
|
|
|
import unittest
|
|
|
|
|
from unittest import TestCase
|
|
|
|
|
|
|
|
|
|
from unittest import TestCase
|
|
|
|
|
|
|
|
|
|
import stripe
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
@ -49,7 +49,7 @@ STRIPE_EVENT_TYPES = ['account.updated', 'account.application.deauthorized', 'ba
|
|
|
|
|
'charge.dispute.created', 'charge.dispute.updated', 'charge.dispute.closed',
|
|
|
|
|
'customer.created', 'customer.updated', 'customer.deleted',
|
|
|
|
|
'customer.card.created', 'customer.card.updated', 'customer.card.deleted',
|
|
|
|
|
'customer.source.created', 'customer.source.deleted', 'customer.source.expiring',
|
|
|
|
|
'customer.source.created', 'customer.source.deleted', 'customer.source.expiring',
|
|
|
|
|
'customer.source.updated',
|
|
|
|
|
'customer.subscription.created', 'customer.subscription.updated',
|
|
|
|
|
'customer.subscription.deleted', 'customer.subscription.trial_will_end',
|
|
|
|
@ -62,7 +62,7 @@ STRIPE_EVENT_TYPES = ['account.updated', 'account.application.deauthorized', 'ba
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# https://stackoverflow.com/questions/2348317/how-to-write-a-pager-for-python-iterators/2350904#2350904
|
|
|
|
|
# https://stackoverflow.com/questions/2348317/how-to-write-a-pager-for-python-iterators/2350904#2350904
|
|
|
|
|
def grouper(iterable, page_size):
|
|
|
|
|
page= []
|
|
|
|
|
for item in iterable:
|
|
|
|
@ -79,7 +79,7 @@ class StripelibError(baseprocessor.ProcessorError):
|
|
|
|
|
|
|
|
|
|
STRIPE_PK = settings.STRIPE_PK
|
|
|
|
|
STRIPE_SK = settings.STRIPE_SK
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# set default stripe api_key to that of unglue.it
|
|
|
|
|
|
|
|
|
|
stripe.api_key = STRIPE_SK
|
|
|
|
@ -96,8 +96,8 @@ stripe.api_version = API_VERSION
|
|
|
|
|
# https://stripe.com/docs/testing
|
|
|
|
|
|
|
|
|
|
TEST_CARDS = (
|
|
|
|
|
('4242424242424242', 'Visa'),
|
|
|
|
|
('4012888888881881', 'Visa'),
|
|
|
|
|
('4242424242424242', 'Visa'),
|
|
|
|
|
('4012888888881881', 'Visa'),
|
|
|
|
|
('5555555555554444', 'MasterCard'),
|
|
|
|
|
('5105105105105100', 'MasterCard'),
|
|
|
|
|
('378282246310005', 'American Express'),
|
|
|
|
@ -132,7 +132,7 @@ CARD_FIELDS_TO_COMPARE = ('exp_month', 'exp_year', 'name', 'address_line1', 'add
|
|
|
|
|
|
|
|
|
|
def filter_none(d):
|
|
|
|
|
return dict([(k,v) for (k,v) in d.items() if v is not None])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# if you create a Customer object, then you'll be able to charge multiple times. You can create a customer with a token.
|
|
|
|
|
|
|
|
|
|
# https://en.wikipedia.org/wiki/Luhn_algorithm#Implementation_of_standard_Mod_10
|
|
|
|
@ -148,18 +148,18 @@ def luhn_checksum(card_number):
|
|
|
|
|
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=1, exp_year=2030, 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": int(exp_month),
|
|
|
|
@ -172,7 +172,7 @@ def card (number=TEST_CARDS[0][0], exp_month=1, exp_year=2030, cvc=None, name=No
|
|
|
|
|
"address_state": address_state,
|
|
|
|
|
"address_country": address_country
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return filter_none(card)
|
|
|
|
|
|
|
|
|
|
def _isListableAPIResource(x):
|
|
|
|
@ -186,10 +186,10 @@ def _isListableAPIResource(x):
|
|
|
|
|
class StripeClient(object):
|
|
|
|
|
def __init__(self, api_key=STRIPE_SK):
|
|
|
|
|
self.api_key = api_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# key entities: Charge, Customer, Token, Event
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def charge(self):
|
|
|
|
|
return stripe.Charge(api_key=self.api_key)
|
|
|
|
|
|
|
|
|
@ -200,22 +200,22 @@ class StripeClient(object):
|
|
|
|
|
@property
|
|
|
|
|
def token(self):
|
|
|
|
|
return stripe.Token(api_key=self.api_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def transfer(self):
|
|
|
|
|
return stripe.Transfer(api_key=self.api_key)
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def event(self):
|
|
|
|
|
return stripe.Event(api_key=self.api_key)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
"""card is a dictionary or a token"""
|
|
|
|
|
# https://stripe.com/docs/api?lang=python#create_customer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
customer = stripe.Customer(api_key=self.api_key).create(
|
|
|
|
|
card=card,
|
|
|
|
|
description=description,
|
|
|
|
@ -224,7 +224,7 @@ class StripeClient(object):
|
|
|
|
|
plan=plan,
|
|
|
|
|
trial_end=trial_end
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# customer.id is useful to save in db
|
|
|
|
|
return customer
|
|
|
|
|
|
|
|
|
@ -234,7 +234,7 @@ class StripeClient(object):
|
|
|
|
|
# customer.id or card required but not both
|
|
|
|
|
# charge the Customer instead of the card
|
|
|
|
|
# amount in cents
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
charge = stripe.Charge(api_key=self.api_key).create(
|
|
|
|
|
amount=int(100*amount), # in cents
|
|
|
|
|
currency=currency,
|
|
|
|
@ -242,15 +242,15 @@ class StripeClient(object):
|
|
|
|
|
card=card,
|
|
|
|
|
description=description
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return charge
|
|
|
|
|
|
|
|
|
|
def refund_charge(self, charge_id):
|
|
|
|
|
# https://stripe.com/docs/api?lang=python#refund_charge
|
|
|
|
|
ch = stripe.Charge(api_key=self.api_key).retrieve(charge_id)
|
|
|
|
|
ch.refund()
|
|
|
|
|
return ch
|
|
|
|
|
|
|
|
|
|
return ch
|
|
|
|
|
|
|
|
|
|
def _all_objs(self, class_type, **kwargs):
|
|
|
|
|
"""a generic iterator for all classes of type stripe.ListableAPIResource"""
|
|
|
|
|
# type=None, created=None, count=None, offset=0
|
|
|
|
@ -263,11 +263,11 @@ class StripeClient(object):
|
|
|
|
|
if _isListableAPIResource(stripe_class):
|
|
|
|
|
kwargs2 = kwargs.copy()
|
|
|
|
|
kwargs2.setdefault('offset', 0)
|
|
|
|
|
kwargs2.setdefault('count', 100)
|
|
|
|
|
|
|
|
|
|
kwargs2.setdefault('count', 100)
|
|
|
|
|
|
|
|
|
|
more_items = True
|
|
|
|
|
while more_items:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
items = stripe_class(api_key=self.api_key).list(**kwargs2)['data']
|
|
|
|
|
for item in items:
|
|
|
|
|
yield item
|
|
|
|
@ -277,12 +277,12 @@ class StripeClient(object):
|
|
|
|
|
more_items = False
|
|
|
|
|
else:
|
|
|
|
|
yield StopIteration
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __getattribute__(self, name):
|
|
|
|
|
""" handle list_* calls"""
|
|
|
|
|
mapping = {'list_charges':"Charge",
|
|
|
|
|
'list_coupons': "Coupon",
|
|
|
|
|
'list_customers':"Customer",
|
|
|
|
|
'list_customers':"Customer",
|
|
|
|
|
'list_events':"Event",
|
|
|
|
|
'list_invoices':"Invoice",
|
|
|
|
|
'list_invoiceitems':"InvoiceItem",
|
|
|
|
@ -293,15 +293,15 @@ class StripeClient(object):
|
|
|
|
|
class_value = mapping[name]
|
|
|
|
|
def list_events(**kwargs):
|
|
|
|
|
for e in self._all_objs(class_value, **kwargs):
|
|
|
|
|
yield e
|
|
|
|
|
return list_events
|
|
|
|
|
yield e
|
|
|
|
|
return list_events
|
|
|
|
|
else:
|
|
|
|
|
return object.__getattribute__(self, name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# can't test Transfer in test mode: "There are no transfers in test mode."
|
|
|
|
|
|
|
|
|
|
#pledge scenario
|
|
|
|
@ -328,7 +328,7 @@ class StripeClient(object):
|
|
|
|
|
|
|
|
|
|
# Errors we still need to catch:
|
|
|
|
|
#
|
|
|
|
|
# * invalid_number -- can't get stripe to generate for us. What it means:
|
|
|
|
|
# * 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
|
|
|
|
|
#
|
|
|
|
@ -352,11 +352,11 @@ class StripeClient(object):
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
@ -368,7 +368,7 @@ class StripeErrorTest(TestCase):
|
|
|
|
|
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
|
|
|
|
@ -380,7 +380,7 @@ class StripeErrorTest(TestCase):
|
|
|
|
|
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)
|
|
|
|
@ -390,8 +390,8 @@ class StripeErrorTest(TestCase):
|
|
|
|
|
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()
|
|
|
|
@ -402,7 +402,7 @@ class StripeErrorTest(TestCase):
|
|
|
|
|
except stripe.error.CardError as e:
|
|
|
|
|
self.assertEqual(e.code, "card_declined")
|
|
|
|
|
self.assertEqual(e.args[0], "Your card was declined")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_charge_bad_cust(self):
|
|
|
|
|
# expect the card to be declined -- and for us to get CardError
|
|
|
|
|
sc = StripeClient()
|
|
|
|
@ -417,24 +417,24 @@ class StripeErrorTest(TestCase):
|
|
|
|
|
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.error.CardError as e:
|
|
|
|
|
self.assertEqual(e.code, "incorrect_number")
|
|
|
|
|
self.assertEqual(e.args[0], "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)
|
|
|
|
@ -448,7 +448,7 @@ class StripeErrorTest(TestCase):
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
@ -459,10 +459,10 @@ class StripeErrorTest(TestCase):
|
|
|
|
|
except stripe.error.CardError as e:
|
|
|
|
|
self.assertEqual(e.code, "invalid_expiry_year")
|
|
|
|
|
self.assertEqual(e.args[0], "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)
|
|
|
|
@ -473,10 +473,10 @@ class StripeErrorTest(TestCase):
|
|
|
|
|
except stripe.error.CardError as e:
|
|
|
|
|
self.assertEqual(e.code, "invalid_cvc")
|
|
|
|
|
self.assertEqual(e.args[0], "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")
|
|
|
|
@ -485,21 +485,21 @@ class StripeErrorTest(TestCase):
|
|
|
|
|
except stripe.error.CardError as e:
|
|
|
|
|
self.assertEqual(e.code, "missing")
|
|
|
|
|
self.assertEqual(e.args[0], "Cannot charge a customer that has no active card")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PledgeScenarioTest(TestCase):
|
|
|
|
|
@classmethod
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
cls._sc = StripeClient(api_key=STRIPE_SK)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# valid card
|
|
|
|
|
card0 = card()
|
|
|
|
|
cls._good_cust = cls._sc.create_customer(card=card0, description="test good customer", email="raymond.yee@gmail.com")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# bad card
|
|
|
|
|
test_card_num_to_get_BAD_ATTACHED_CARD = ERROR_TESTING['BAD_ATTACHED_CARD'][0]
|
|
|
|
|
card1 = card(number=test_card_num_to_get_BAD_ATTACHED_CARD)
|
|
|
|
|
cls._cust_bad_card = cls._sc.create_customer(card=card1, description="test bad customer", email="rdhyee@gluejar.com")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_charge_good_cust(self):
|
|
|
|
|
charge = self._sc.create_charge(10, customer=self._good_cust.id, description="$10 for good cust")
|
|
|
|
|
self.assertEqual(type(charge.id), str)
|
|
|
|
@ -507,39 +507,39 @@ class PledgeScenarioTest(TestCase):
|
|
|
|
|
# print out all the pieces of Customer and Charge objects
|
|
|
|
|
print(dir(charge))
|
|
|
|
|
print(dir(self._good_cust))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_error_creating_customer_with_declined_card(self):
|
|
|
|
|
# should get a CardError upon attempt to create Customer with this card
|
|
|
|
|
_card = card(number=card(ERROR_TESTING['CHARGE_DECLINE'][0]))
|
|
|
|
|
self.assertRaises(stripe.error.CardError, self._sc.create_customer, card=_card)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_charge_bad_cust(self):
|
|
|
|
|
# expect the card to be declined -- and for us to get CardError
|
|
|
|
|
self.assertRaises(stripe.error.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
|
|
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#cls._good_cust.delete()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StripePaymentRequest(baseprocessor.BasePaymentRequest):
|
|
|
|
|
"""so far there is no need to have a separate class here"""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
class Processor(baseprocessor.Processor):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def make_account(self, user=None, token=None, email=None):
|
|
|
|
|
"""returns a payment.models.Account based on stripe token and user"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if token is None or len(token) == 0:
|
|
|
|
|
raise StripelibError("input token is None", None)
|
|
|
|
|
|
|
|
|
|
sc = StripeClient()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# create customer and charge id and then charge the customer
|
|
|
|
|
try:
|
|
|
|
|
if user:
|
|
|
|
@ -549,7 +549,7 @@ class Processor(baseprocessor.Processor):
|
|
|
|
|
customer = sc.create_customer(card=token, description='anonymous user', email=email)
|
|
|
|
|
except stripe.error.StripeError as e:
|
|
|
|
|
raise StripelibError(e.args, e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
account = Account(host = PAYMENT_HOST_STRIPE,
|
|
|
|
|
account_id = customer.id,
|
|
|
|
|
card_last4 = customer.active_card.last4,
|
|
|
|
@ -562,147 +562,147 @@ class Processor(baseprocessor.Processor):
|
|
|
|
|
)
|
|
|
|
|
if user and user.profile.account:
|
|
|
|
|
user.profile.account.deactivate()
|
|
|
|
|
account.save()
|
|
|
|
|
account.recharge_failed_transactions()
|
|
|
|
|
account.save()
|
|
|
|
|
account.recharge_failed_transactions()
|
|
|
|
|
else:
|
|
|
|
|
account.save()
|
|
|
|
|
return account
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Preapproval(StripePaymentRequest, baseprocessor.Processor.Preapproval):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__( self, transaction, amount, expiry=None, return_url=None, paymentReason=""):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# set the expiration date for the preapproval if not passed in. This is what the paypal library does
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.transaction = transaction
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
now_val = now()
|
|
|
|
|
if expiry is None:
|
|
|
|
|
expiry = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD )
|
|
|
|
|
transaction.date_authorized = now_val
|
|
|
|
|
transaction.date_expired = expiry
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# let's figure out what part of transaction can be used to store info
|
|
|
|
|
# try placing charge id in transaction.pay_key
|
|
|
|
|
# need to set amount
|
|
|
|
|
# how does transaction.max_amount get set? -- coming from /pledge/xxx/ -> manager.process_transaction
|
|
|
|
|
# max_amount is set -- but I don't think we need it for stripe
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ASSUMPTION: a user has any given moment one and only one active payment Account
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
account = transaction.user.profile.account
|
|
|
|
|
if not account:
|
|
|
|
|
logger.warning("user {0} has no active payment account".format(transaction.user))
|
|
|
|
|
raise StripelibError("user {0} has no active payment account".format(transaction.user))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("user: {0} customer.id is {1}".format(transaction.user, account.account_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# settings to apply to transaction for TRANSACTION_STATUS_ACTIVE
|
|
|
|
|
# should approved be set to False and wait for a webhook?
|
|
|
|
|
transaction.approved = True
|
|
|
|
|
transaction.type = PAYMENT_TYPE_AUTHORIZATION
|
|
|
|
|
transaction.host = PAYMENT_HOST_STRIPE
|
|
|
|
|
transaction.status = TRANSACTION_STATUS_ACTIVE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transaction.preapproval_key = account.account_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transaction.currency = 'USD'
|
|
|
|
|
transaction.amount = amount
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transaction.save()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def key(self):
|
|
|
|
|
return self.transaction.preapproval_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def next_url(self):
|
|
|
|
|
"""return None because no redirection to stripe is required"""
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Pay(StripePaymentRequest, baseprocessor.Processor.Pay):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
The pay function generates a redirect URL to approve the transaction
|
|
|
|
|
If the transaction has a null user (is_anonymous), then a token musr be supplied
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__( self, transaction, return_url=None, amount=None, paymentReason="", token=None):
|
|
|
|
|
self.transaction=transaction
|
|
|
|
|
self.url = return_url
|
|
|
|
|
|
|
|
|
|
now_val = now()
|
|
|
|
|
|
|
|
|
|
now_val = now()
|
|
|
|
|
transaction.date_authorized = now_val
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ASSUMPTION: a user has any given moment one and only one active payment Account
|
|
|
|
|
if token:
|
|
|
|
|
# user is anonymous
|
|
|
|
|
account = transaction.get_payment_class().make_account(token = token, email = transaction.receipt)
|
|
|
|
|
else:
|
|
|
|
|
account = transaction.user.profile.account
|
|
|
|
|
|
|
|
|
|
account = transaction.user.profile.account
|
|
|
|
|
|
|
|
|
|
if not account:
|
|
|
|
|
logger.warning("user {0} has no active payment account".format(transaction.user))
|
|
|
|
|
raise StripelibError("user {0} has no active payment account".format(transaction.user))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
logger.info("user: {0} customer.id is {1}".format(transaction.user, account.account_id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# settings to apply to transaction for TRANSACTION_STATUS_ACTIVE
|
|
|
|
|
# should approved be set to False and wait for a webhook?
|
|
|
|
|
transaction.approved = True
|
|
|
|
|
transaction.type = PAYMENT_TYPE_INSTANT
|
|
|
|
|
transaction.host = PAYMENT_HOST_STRIPE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transaction.preapproval_key = account.account_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transaction.currency = 'USD'
|
|
|
|
|
transaction.amount = amount
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transaction.save()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# execute the transaction
|
|
|
|
|
p = transaction.get_payment_class().Execute(transaction)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if p.success() and not p.error():
|
|
|
|
|
transaction.pay_key = p.key()
|
|
|
|
|
transaction.save()
|
|
|
|
|
else:
|
|
|
|
|
self.errorMessage = p.errorMessage #pass error message up
|
|
|
|
|
logger.info("execute_transaction Error: {}".format(p.error_string()))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def amount( self ):
|
|
|
|
|
return self.transaction.amount
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def key( self ):
|
|
|
|
|
return self.transaction.pay_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def next_url( self ):
|
|
|
|
|
return self.url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Execute(StripePaymentRequest):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
The Execute function attempts to charge the credit card of stripe Customer associated with user connected to transaction.
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, transaction=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.transaction = transaction
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# make sure transaction hasn't already been executed
|
|
|
|
|
if transaction.status == TRANSACTION_STATUS_COMPLETE:
|
|
|
|
|
return
|
|
|
|
|
# make sure we are dealing with a stripe transaction
|
|
|
|
|
if transaction.host != PAYMENT_HOST_STRIPE:
|
|
|
|
|
raise StripelibError("transaction.host {0} is not the expected {1}".format(transaction.host, PAYMENT_HOST_STRIPE))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sc = StripeClient()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# look first for transaction.user.profile.account.account_id
|
|
|
|
|
try:
|
|
|
|
|
customer_id = transaction.user.profile.account.account_id
|
|
|
|
|
except:
|
|
|
|
|
customer_id = transaction.preapproval_key
|
|
|
|
|
|
|
|
|
|
if customer_id is not None:
|
|
|
|
|
|
|
|
|
|
if customer_id is not None:
|
|
|
|
|
try:
|
|
|
|
|
# useful things to put in description: transaction.id, transaction.user.id, customer_id, transaction.amount
|
|
|
|
|
charge = sc.create_charge(transaction.amount, customer=customer_id,
|
|
|
|
@ -718,8 +718,8 @@ class Processor(baseprocessor.Processor):
|
|
|
|
|
r = PaymentResponse.objects.create(api="stripelib.Execute", correlation_id=None,
|
|
|
|
|
timestamp=now(), info=e.args[0],
|
|
|
|
|
status=TRANSACTION_STATUS_ERROR, transaction=transaction)
|
|
|
|
|
|
|
|
|
|
transaction.status = TRANSACTION_STATUS_ERROR
|
|
|
|
|
|
|
|
|
|
transaction.status = TRANSACTION_STATUS_ERROR
|
|
|
|
|
self.errorMessage = e.args # manager puts this on transaction
|
|
|
|
|
transaction.save()
|
|
|
|
|
|
|
|
|
@ -730,57 +730,57 @@ class Processor(baseprocessor.Processor):
|
|
|
|
|
# otherwise, report exception to us
|
|
|
|
|
else:
|
|
|
|
|
logger.exception("transaction id {0}, exception: {1}".format(transaction.id, e.args))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
self.charge = charge
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transaction.status = TRANSACTION_STATUS_COMPLETE
|
|
|
|
|
transaction.pay_key = charge.id
|
|
|
|
|
transaction.date_payment = now()
|
|
|
|
|
transaction.save()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# fire signal for sucessful transaction
|
|
|
|
|
transaction_charged.send(sender=self, transaction=transaction)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
# nothing to charge
|
|
|
|
|
raise StripelibError("No customer id available to charge for transaction {0}".format(transaction.id), None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def api(self):
|
|
|
|
|
return "Base Pay"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def key(self):
|
|
|
|
|
# IN paypal land, our key is updated from a preapproval to a pay key here, just return the existing key
|
|
|
|
|
return self.transaction.pay_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PreapprovalDetails(StripePaymentRequest):
|
|
|
|
|
'''
|
|
|
|
|
Get details about an authorized token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
This api must set 4 different class variables to work with the code in manager.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
status - one of the global transaction status codes
|
|
|
|
|
approved - boolean value
|
|
|
|
|
currency - not used in this API, but we can get some more info via other APIs - TODO
|
|
|
|
|
amount - not used in this API, but we can get some more info via other APIs - TODO
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
def __init__(self, transaction):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.transaction = transaction
|
|
|
|
|
self.status = self.transaction.status
|
|
|
|
|
if self.status == TRANSACTION_STATUS_CANCELED:
|
|
|
|
|
self.approved = False
|
|
|
|
|
else:
|
|
|
|
|
self.approved = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Set the other fields that are expected. We don't have values for these now, so just copy the transaction
|
|
|
|
|
self.currency = transaction.currency
|
|
|
|
|
self.amount = transaction.amount
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ProcessIPN(self, request):
|
|
|
|
|
# retrieve the request's body and parse it as JSON in, e.g. Django
|
|
|
|
|
try:
|
|
|
|
@ -793,7 +793,7 @@ class Processor(baseprocessor.Processor):
|
|
|
|
|
# now parse out pieces of the webhook
|
|
|
|
|
event_id = event_json.get("id")
|
|
|
|
|
# use Stripe to ask for details -- ignore what we're sent for security
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
sc = StripeClient()
|
|
|
|
|
try:
|
|
|
|
|
event = sc.event.retrieve(event_id)
|
|
|
|
@ -814,20 +814,20 @@ class Processor(baseprocessor.Processor):
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("Parsing of event_type into resource, action failed: {0}".format(e))
|
|
|
|
|
return HttpResponse(status=400)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
ev_object = event.data.object
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning("attempt to retrieve event object failed: {0}".format(e))
|
|
|
|
|
logger.warning("attempt to retrieve event object failed: {0}".format(e))
|
|
|
|
|
return HttpResponse(status=400)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if event_type == 'account.updated':
|
|
|
|
|
# should we alert ourselves?
|
|
|
|
|
# how about account.application.deauthorized ?
|
|
|
|
|
pass
|
|
|
|
|
elif resource == 'charge':
|
|
|
|
|
# we need to handle: succeeded, failed, refunded, disputed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if action == 'succeeded':
|
|
|
|
|
# TO DO: delete this logic since we don't do anything but look up transaction.
|
|
|
|
|
logger.info("charge.succeeded webhook for {0}".format(ev_object.get("id")))
|
|
|
|
@ -843,8 +843,8 @@ class Processor(baseprocessor.Processor):
|
|
|
|
|
else:
|
|
|
|
|
logger.warning("ev_object.id {0} != transaction.pay_key {1}".format(ev_object.id, transaction.pay_key))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(e)
|
|
|
|
|
|
|
|
|
|
logger.warning(e)
|
|
|
|
|
|
|
|
|
|
elif action == 'failed':
|
|
|
|
|
# TO DO: delete this logic since we don't do anything but look up transaction.
|
|
|
|
|
logger.info("charge.failed webhook for {0}".format(ev_object.get("id")))
|
|
|
|
@ -863,7 +863,7 @@ class Processor(baseprocessor.Processor):
|
|
|
|
|
elif action == 'refunded':
|
|
|
|
|
pass
|
|
|
|
|
elif action == 'disputed':
|
|
|
|
|
pass
|
|
|
|
|
pass
|
|
|
|
|
else:
|
|
|
|
|
# unexpected
|
|
|
|
|
pass
|
|
|
|
@ -884,12 +884,12 @@ class Processor(baseprocessor.Processor):
|
|
|
|
|
pass
|
|
|
|
|
else: # other events
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return HttpResponse("event_id: {0} event_type: {1}".format(event_id, event_type))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def suite():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
testcases = [PledgeScenarioTest, StripeErrorTest]
|
|
|
|
|
#testcases = [StripeErrorTest]
|
|
|
|
|
suites = unittest.TestSuite([unittest.TestLoader().loadTestsFromTestCase(testcase) for testcase in testcases])
|
|
|
|
@ -908,4 +908,4 @@ if __name__ == '__main__':
|
|
|
|
|
#unittest.main()
|
|
|
|
|
suites = suite()
|
|
|
|
|
#suites = unittest.defaultTestLoader.loadTestsFromModule(__import__('__main__'))
|
|
|
|
|
unittest.TextTestRunner().run(suites)
|
|
|
|
|
unittest.TextTestRunner().run(suites)
|
|
|
|
|