Merge pull request #190 from Gluejar/instant_charge

Instant charge:  implementing PaymentManager.charge
pull/1/head
Raymond Yee 2013-07-19 16:12:03 -07:00
commit 7c013ef93b
4 changed files with 170 additions and 24 deletions

View File

@ -96,6 +96,8 @@ class Processor:
def __init__( self, transaction, return_url=None, amount=None, paymentReason=""):
self.transaction=transaction
self.return_url = return_url
self.amount = amount
def api(self):
return "null api"
@ -104,13 +106,13 @@ class Processor:
return None
def amount( self ):
return None
return self.amount
def key( self ):
return None
def next_url( self ):
return self.url
return self.return_url
class Preapproval(Pay):

View File

@ -554,8 +554,8 @@ class PaymentManager( object ):
authorizes a set amount of money to be collected at a later date
return_url: url to redirect supporter to after a successful PayPal transaction
paymentReason: a memo line that will show up in the Payer's Amazon (and Paypal?) account
return_url: url to redirect supporter to after a successful transaction
paymentReason: a memo line that will show up in the unglue.it accounting
modification: whether this authorize call is part of a modification of an existing pledge
return value: a tuple of the new transaction object and a re-direct url. If the process fails,
@ -626,6 +626,73 @@ class PaymentManager( object ):
transaction.save()
logger.info("Authorize Error: " + p.error_string())
return transaction, None
def charge(self, transaction, return_url=None, paymentReason="unglue.it Purchase"):
'''
charge
immediately attempt to collect on transaction
return_url: url to redirect supporter to after a successful transaction
paymentReason: a memo line that will show up in our stripe accounting
return value: a tuple of the new transaction object and a re-direct url. If the process fails,
the redirect url will be None
'''
if transaction.host == PAYMENT_HOST_NONE:
#TODO send user to select a payment processor -- for now, set to a system setting
transaction.host = settings.PAYMENT_PROCESSOR
# we might want to not allow for a return_url to be passed in but calculated
# here because we have immediate access to the Transaction object.
#if return_url is None:
#
# #return_path = "{0}?{1}".format(reverse('charge_complete'),
# # urllib.urlencode({'tid':transaction.id}))
# return_path = "{0}?{1}".format(reverse('pledge_complete'),
# urllib.urlencode({'tid':transaction.id}))
# return_url = urlparse.urljoin(settings.BASE_URL_SECURE, return_path)
# Question: do I need to set transaction.amount = transaction.max_amount ?
p = transaction.get_payment_class().Pay(transaction, amount=transaction.max_amount, return_url=return_url, paymentReason=paymentReason)
# Create a response for this
#envelope = p.envelope()
#
#if envelope:
# r = PaymentResponse.objects.create(api=p.url,
# correlation_id = p.correlation_id(),
# timestamp = p.timestamp(),
# info = p.raw_response,
# transaction=transaction)
if p.success() and not p.error():
transaction.preapproval_key = p.key()
transaction.save()
# it make sense for the payment processor library to calculate next_url when
# user is redirected there. But if no redirection is required, send user
# straight on to the return_url
url = p.next_url()
if url is None:
url = return_url
logger.info("Pay Success: " + url if url is not None else '')
return transaction, url
else:
transaction.error = p.error_string()
transaction.save()
logger.info("Pay Error: " + p.error_string())
return transaction, None
def process_transaction(self, currency, amount, host=PAYMENT_HOST_NONE, campaign=None, user=None,
return_url=None, paymentReason="unglue.it Pledge", pledge_extra=None,

View File

@ -31,6 +31,7 @@ from regluit.payment.parameters import (
TRANSACTION_STATUS_COMPLETE,
TRANSACTION_STATUS_ERROR,
PAYMENT_TYPE_AUTHORIZATION,
PAYMENT_TYPE_INSTANT,
TRANSACTION_STATUS_CANCELED
)
from regluit.payment.signals import transaction_charged, transaction_failed
@ -522,7 +523,7 @@ class PledgeScenarioTest(TestCase):
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)
self.assertEqual(type(charge.id), unicode)
# print out all the pieces of Customer and Charge objects
print dir(charge)
@ -542,17 +543,19 @@ class PledgeScenarioTest(TestCase):
def tearDownClass(cls):
# clean up stuff we create in test -- right now list current objects
pass
#cls._good_cust.delete()
print "list of customers"
print [(i, c.id, c.description, c.email, datetime.fromtimestamp(c.created, tz=utc), c.account_balance, c.delinquent, c.active_card.fingerprint, c.active_card.type, c.active_card.last4, c.active_card.exp_month, c.active_card.exp_year, c.active_card.country) for(i, c) in enumerate(cls._sc.customer.all()["data"])]
print "list of charges"
print [(i, c.id, c.amount, c.amount_refunded, c.currency, c.description, datetime.fromtimestamp(c.created, tz=utc), c.paid, c.fee, c.disputed, c.amount_refunded, c.failure_message, c.card.fingerprint, c.card.type, c.card.last4, c.card.exp_month, c.card.exp_year) for (i, c) in enumerate(cls._sc.charge.all()['data'])]
# can retrieve events since a certain time?
print "list of events", cls._sc.event.all()
print [(i, e.id, e.type, e.created, e.pending_webhooks, e.data) for (i,e) in enumerate(cls._sc.event.all()['data'])]
#print "list of customers"
#print [(i, c.id, c.description, c.email, datetime.fromtimestamp(c.created, tz=utc), c.account_balance, c.delinquent, c.active_card.fingerprint, c.active_card.type, c.active_card.last4, c.active_card.exp_month, c.active_card.exp_year, c.active_card.country) for(i, c) in enumerate(cls._sc.customer.all()["data"])]
#
#print "list of charges"
#print [(i, c.id, c.amount, c.amount_refunded, c.currency, c.description, datetime.fromtimestamp(c.created, tz=utc), c.paid, c.fee, c.disputed, c.amount_refunded, c.failure_message, c.card.fingerprint, c.card.type, c.card.last4, c.card.exp_month, c.card.exp_year) for (i, c) in enumerate(cls._sc.charge.all()['data'])]
#
## can retrieve events since a certain time?
#print "list of events", cls._sc.event.all()
#print [(i, e.id, e.type, e.created, e.pending_webhooks, e.data) for (i,e) in enumerate(cls._sc.event.all()['data'])]
class StripePaymentRequest(baseprocessor.BasePaymentRequest):
"""so far there is no need to have a separate class here"""
@ -647,19 +650,51 @@ class Processor(baseprocessor.Processor):
'''
def __init__( self, transaction, return_url=None, amount=None, paymentReason=""):
self.transaction=transaction
self.transaction=transaction
self.url = return_url
#def api(self):
# return "null api"
#
##def exec_status( self ):
# return None
now_val = now()
transaction.date_authorized = now_val
# 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_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:
transaction.error = p.error_string()
transaction.save()
logger.info("execute_transaction Error: " + p.error_string())
def amount( self ):
return self.transaction.amount
def key( self ):
return None
return self.transaction.pay_key
def next_url( self ):
return self.url
@ -740,7 +775,6 @@ class Processor(baseprocessor.Processor):
# 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
@ -874,8 +908,8 @@ class Processor(baseprocessor.Processor):
def suite():
#testcases = [PledgeScenarioTest, StripeErrorTest]
testcases = [StripeErrorTest]
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

View File

@ -297,6 +297,49 @@ class TransactionTest(TestCase):
self.assertEqual(results[0].amount, D('12.34'))
self.assertEqual(c.left,c.target-D('12.34'))
self.assertEqual(c.supporters_count, 1)
def test_paymentmanager_charge(self):
"""
test regluit.payment.manager.PaymentManager.charge
trying to simulate the conditions of having a bare transaction setup before we try to do
an instant charge.
"""
user = User.objects.create_user('pm_charge', 'support2@example.org', 'payment_test')
# need to create an Account to associate with user
from regluit.payment.stripelib import StripeClient, card, Processor
sc = StripeClient()
# valid card and Account
card0 = card()
stripe_processor = Processor()
account = stripe_processor.make_account(user,token=card0)
w = Work()
w.save()
c = Campaign(target=D('1000.00'),deadline=now() + timedelta(days=180),work=w)
c.save()
t = Transaction(host='stripelib')
t.max_amount = D('12.34')
t.type = PAYMENT_TYPE_NONE
t.status = TRANSACTION_STATUS_NONE
t.campaign = c
t.approved = False
t.user = user
t.save()
pm = PaymentManager()
response = pm.charge(t)
self.assertEqual(t.status, TRANSACTION_STATUS_COMPLETE)
self.assertEqual(t.type, EXECUTE_TYPE_CHAINED_INSTANT)
self.assertEqual(t.amount, D('12.34'))
class BasicGuiTest(TestCase):
def setUp(self):