regluit/payment/stripelib.py

943 lines
40 KiB
Python

# https://github.com/stripe/stripe-python
# https://stripe.com/docs/api?lang=python#top
"""
external library imports
"""
import logging
import json
import re
import stripe
from datetime import datetime, timedelta
from itertools import islice
from pytz import utc
"""
django imports
"""
from django.conf import settings
from django.core.mail import send_mail
from django.http import HttpResponse
"""
regluit imports
"""
from regluit.payment import baseprocessor
from regluit.payment.models import Account, Transaction, PaymentResponse
from regluit.payment.parameters import (
PAYMENT_HOST_STRIPE,
TRANSACTION_STATUS_ACTIVE,
TRANSACTION_STATUS_COMPLETE,
TRANSACTION_STATUS_ERROR,
PAYMENT_TYPE_AUTHORIZATION,
PAYMENT_TYPE_INSTANT,
TRANSACTION_STATUS_CANCELED
)
from regluit.payment.signals import transaction_charged, transaction_failed
from regluit.utils.localdatetime import now, zuluformat
# as of 2013.07.15
# ['charge.disputed', 'coupon.updated'] are legacy events -- don't know whether to
# include them in list
STRIPE_EVENT_TYPES = ['account.updated', 'account.application.deauthorized', 'balance.available',
'charge.succeeded', 'charge.failed', 'charge.refunded', 'charge.captured',
'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.subscription.created', 'customer.subscription.updated',
'customer.subscription.deleted', 'customer.subscription.trial_will_end',
'customer.discount.created', 'customer.discount.updated',
'customer.discount.deleted', 'invoice.created', 'invoice.updated',
'invoice.payment_succeeded', 'invoice.payment_failed', 'invoiceitem.created',
'invoiceitem.updated', 'invoiceitem.deleted', 'plan.created', 'plan.updated',
'plan.deleted', 'coupon.created', 'coupon.deleted', 'transfer.created',
'transfer.updated', 'transfer.paid', 'transfer.failed', 'ping']
logger = logging.getLogger(__name__)
# http://stackoverflow.com/questions/2348317/how-to-write-a-pager-for-python-iterators/2350904#2350904
def grouper(iterable, page_size):
page= []
for item in iterable:
page.append( item )
if len(page) == page_size:
yield page
page= []
if len(page):
yield page
class StripelibError(baseprocessor.ProcessorError):
pass
try:
import unittest
from unittest import TestCase
except:
from django.test import TestCase
from django.utils import unittest
# if customer.id doesn't exist, create one and then charge the customer
# we probably should ask our users whether they are ok with our creating a customer id account -- or ask for credit
# card info each time....
# should load the keys for Stripe from db -- but for now just hardcode here
# moving towards not having the stripe api key for the non profit partner in the unglue.it code -- but in a logically
# distinct application
TEST_STRIPE_PK = 'pk_0EajXPn195ZdF7Gt7pCxsqRhNN5BF'
TEST_STRIPE_SK = 'sk_0EajIO4Dnh646KPIgLWGcO10f9qnH'
try:
from regluit.core.models import Key
STRIPE_PK = Key.objects.get(name="STRIPE_PK").value
STRIPE_SK = Key.objects.get(name="STRIPE_SK").value
logger.info('Successful loading of STRIPE_*_KEYs')
except Exception, e:
# currently test keys for Gluejar and for raymond.yee@gmail.com as standin for non-profit
logger.info('Exception {0} Need to use TEST STRIPE_*_KEYs'.format(e))
STRIPE_PK = TEST_STRIPE_PK
STRIPE_SK = TEST_STRIPE_SK
# set default stripe api_key to that of unglue.it
stripe.api_key = STRIPE_SK
# maybe we should be able to set this in django.settings...
# to start with, let's try hard-coding the api_version
# https://stripe.com/docs/upgrades?since=2012-07-09#api-changelog
#API_VERSION = '2012-07-09'
API_VERSION = '2013-02-13'
stripe.api_version = API_VERSION
# https://stripe.com/docs/testing
TEST_CARDS = (
('4242424242424242', 'Visa'),
('4012888888881881', 'Visa'),
('5555555555554444', 'MasterCard'),
('5105105105105100', 'MasterCard'),
('378282246310005', 'American Express'),
('371449635398431', 'American Express'),
('6011111111111117', 'Discover'),
('6011000990139424', 'Discover'),
('30569309025904', "Diner's Club"),
('38520000023237', "Diner's Club"),
('3530111333300000', 'JCB'),
('3566002020360505','JCB')
)
ERROR_TESTING = dict((
('ADDRESS1_ZIP_FAIL', ('4000000000000010', 'address_line1_check and address_zip_check will both fail')),
('ADDRESS1_FAIL', ('4000000000000028', 'address_line1_check will fail.')),
('ADDRESS_ZIP_FAIL', ('4000000000000036', 'address_zip_check will fail.')),
('CVC_CHECK_FAIL', ('4000000000000101', 'cvc_check will fail.')),
('BAD_ATTACHED_CARD', ('4000000000000341', 'Attaching this card to a Customer object will succeed, but attempts to charge the customer will fail.')),
('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.
#incorrect_number: Use a number that fails the Luhn check, e.g. 4242424242424241.
#invalid_expiry_month: Use an invalid month e.g. 13.
#invalid_expiry_year: Use a year in the past e.g. 1970.
#invalid_cvc: Use a two digit number e.g. 99.
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.
# 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=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": 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,
"address_zip": address_zip,
"address_state": address_state,
"address_country": address_country
}
return filter_none(card)
def _isListableAPIResource(x):
"""test whether x is an instance of the stripe.ListableAPIResource class"""
try:
return issubclass(x, stripe.ListableAPIResource)
except:
return False
class StripeClient(object):
def __init__(self, api_key=STRIPE_SK):
self.api_key = api_key
# key entities: Charge, Customer, Token, Event
@property
def charge(self):
return stripe.Charge(api_key=self.api_key)
@property
def customer(self):
return stripe.Customer(api_key=self.api_key)
@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
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,
email=email,
account_balance=account_balance,
plan=plan,
trial_end=trial_end
)
# customer.id is useful to save in db
return customer
def create_charge(self, amount, currency="usd", customer=None, card=None, description=None ):
# https://stripe.com/docs/api?lang=python#create_charge
# 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,
customer=customer,
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
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
# obj_type: one of 'Charge','Coupon','Customer', 'Event','Invoice', 'InvoiceItem', 'Plan', 'Transfer'
try:
stripe_class = getattr(stripe, class_type)
except:
yield StopIteration
else:
if _isListableAPIResource(stripe_class):
kwargs2 = kwargs.copy()
kwargs2.setdefault('offset', 0)
kwargs2.setdefault('count', 100)
more_items = True
while more_items:
items = stripe_class(api_key=self.api_key).all(**kwargs2)['data']
for item in items:
yield item
if len(items):
kwargs2['offset'] += len(items)
else:
more_items = False
else:
yield StopIteration
def __getattribute__(self, name):
""" handle list_* calls"""
mapping = {'list_charges':"Charge",
'list_coupons': "Coupon",
'list_customers':"Customer",
'list_events':"Event",
'list_invoices':"Invoice",
'list_invoiceitems':"InvoiceItem",
'list_plans':"Plan",
'list_transfers':"Transfer"
}
if name in mapping.keys():
class_value = mapping[name]
def list_events(**kwargs):
for e in self._all_objs(class_value, **kwargs):
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
# bad card -- what types of erros to handle?
# https://stripe.com/docs/api#errors
# 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
# especially: charge.disputed
# I think following (charge.succeeded, charge.failed, charge.refunded) pretty much sychronous to our actions
# customer.created, customer.updated, customer.deleted
# transfer
# I expect the ones related to transfers all happen asynchronously: transfer.created, transfer.updated, transfer.failed
# When will the money I charge with Stripe end up in my bank account?
# Every day, we transfer the money that you charged seven days previously?that is, you receive the money for your March 1st charges on March 8th.
# pending payments?
# 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_")
# dispute, failure_message, fee, fee_details
self.assertEqual(charge1.dispute,None)
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):
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), unicode)
# 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.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.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()
#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"""
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:
customer = sc.create_customer(card=token, description=user.username,
email=user.email)
else:
customer = sc.create_customer(card=token, description='anonymous user', email=email)
except stripe.StripeError as e:
raise StripelibError(e.message, e)
account = Account(host = PAYMENT_HOST_STRIPE,
account_id = customer.id,
card_last4 = customer.active_card.last4,
card_type = customer.active_card.type,
card_exp_month = customer.active_card.exp_month,
card_exp_year = customer.active_card.exp_year,
card_fingerprint = customer.active_card.fingerprint,
card_country = customer.active_card.country,
user = user
)
if user and user.profile.account:
user.profile.account.deactivate()
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()
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
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: " + 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:
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,
description=json.dumps({"t.id":transaction.id,
"email":transaction.user.email if transaction.user else transaction.receipt,
"cus.id":customer_id,
"tc.id": transaction.campaign.id,
"amount": float(transaction.amount)}))
except stripe.StripeError as e:
# what to record in terms of errors? (error log?)
# use PaymentResponse to store error
r = PaymentResponse.objects.create(api="stripelib.Execute", correlation_id=None,
timestamp=now(), info=e.message,
status=TRANSACTION_STATUS_ERROR, transaction=transaction)
transaction.status = TRANSACTION_STATUS_ERROR
self.errorMessage = e.message # manager puts this on transaction
transaction.save()
# fire off the fact that transaction failed -- should actually do so only if not a transient error
# if card_declined or expired card, ask user to update account
if isinstance(e, stripe.CardError) and e.code in ('card_declined', 'expired_card', 'incorrect_number', 'processing_error'):
transaction_failed.send(sender=self, transaction=transaction)
# otherwise, report exception to us
else:
logger.exception("transaction id {0}, exception: {1}".format(transaction.id, e.message))
# raise StripelibError(e.message, e)
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:
event_json = json.loads(request.body)
except ValueError, e:
# not able to parse request.body -- throw a "Bad Request" error
logger.warning("Non-json being sent to Stripe IPN: {0}".format(e))
return HttpResponse(status=400)
else:
# 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)
except stripe.InvalidRequestError:
logger.warning("Invalid Event ID: {0}".format(event_id))
return HttpResponse(status=400)
else:
event_type = event.get("type")
if event_type not in STRIPE_EVENT_TYPES:
logger.warning("Unrecognized Stripe event type {0} for event {1}".format(event_type, event_id))
# is this the right code to respond with?
return HttpResponse(status=400)
# https://stripe.com/docs/api?lang=python#event_types -- type to delegate things
# parse out type as resource.action
try:
(resource, action) = re.match("^(.+)\.([^\.]*)$", event_type).groups()
except Exception, 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, 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")))
# try to parse description of object to pull related transaction if any
# wrapping this in a try statement because it possible that we have a charge.succeeded outside context of unglue.it
try:
charge_meta = json.loads(ev_object["description"])
transaction = Transaction.objects.get(id=charge_meta["t.id"])
# now check that account associated with the transaction matches
# ev.data.object.id, t.pay_key
if ev_object.id == transaction.pay_key:
logger.info("ev_object.id == transaction.pay_key: {0}".format(ev_object.id))
else:
logger.warning("ev_object.id {0} <> transaction.pay_key {1}".format(ev_object.id, transaction.pay_key))
except Exception, 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")))
try:
charge_meta = json.loads(ev_object["description"])
transaction = Transaction.objects.get(id=charge_meta["t.id"])
# now check that account associated with the transaction matches
# ev.data.object.id, t.pay_key
if ev_object.id == transaction.pay_key:
logger.info("ev_object.id == transaction.pay_key: {0}".format(ev_object.id))
else:
logger.warning("ev_object.id {0} <> transaction.pay_key {1}".format(ev_object.id, transaction.pay_key))
except Exception, e:
logger.warning(e)
elif action == 'refunded':
pass
elif action == 'disputed':
pass
else:
# unexpected
pass
elif resource == 'customer':
if action == 'created':
# test application: email Raymond
# do we have a flag to indicate production vs non-production? -- or does it matter?
# email RY whenever a new Customer created -- we probably want to replace this with some other
# more useful long tem action.
send_mail("Stripe Customer (id {0}; description: {1}) created".format(ev_object.get("id"), ev_object.get("description")),
"Stripe Customer email: {0}".format(ev_object.get("email")),
"notices@gluejar.com",
["rdhyee@gluejar.com"])
logger.info("email sent for customer.created for {0}".format(ev_object.get("id")))
# handle updated, deleted
else:
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])
#suites.addTest(LibraryThingTest('test_cache'))
#suites.addTest(SettingsTest('test_dev_me_alignment')) # give option to test this alignment
return suites
# IPNs/webhooks: https://stripe.com/docs/webhooks
# how to use pending_webhooks ?
# all events
# https://stripe.com/docs/api?lang=python#list_events
if __name__ == '__main__':
#unittest.main()
suites = suite()
#suites = unittest.defaultTestLoader.loadTestsFromModule(__import__('__main__'))
unittest.TextTestRunner().run(suites)