From cf433386e8d33a52d7be4cc1aeef3fb60c201c0b Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Mon, 15 Oct 2012 06:24:12 -0700
Subject: [PATCH 01/33] starting to make it easier to put stripelib in test
mode
---
payment/stripelib.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index b48efd2b..ae99e8b0 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -35,6 +35,9 @@ except:
# 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
From 8a85aa88d3ce98fb5bec4e035907fe693e008f9a Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Thu, 18 Oct 2012 06:59:08 -0700
Subject: [PATCH 02/33] first cut at producing iterator interfaces to Stripe
objects -- here I implement one for events don't yield empty page in
bookdata.py's grouper remove extraneous import in gutenberg.py expose stripe
test key as a module variable to make it easier to create a StripeClient that
will be in test mode (sc=StipeClient(api_key=TEST_STRIPE_SK))
---
experimental/bookdata.py | 3 ++-
experimental/gutenberg/gutenberg.py | 1 -
payment/stripelib.py | 37 +++++++++++++++++++++++++----
3 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/experimental/bookdata.py b/experimental/bookdata.py
index 335950d0..ebfbf46e 100644
--- a/experimental/bookdata.py
+++ b/experimental/bookdata.py
@@ -58,7 +58,8 @@ def grouper(iterable, page_size):
if len(page) == page_size:
yield page
page= []
- yield page
+ if len(page):
+ yield page
class FreebaseException(Exception):
pass
diff --git a/experimental/gutenberg/gutenberg.py b/experimental/gutenberg/gutenberg.py
index bbbafd1f..db556a36 100644
--- a/experimental/gutenberg/gutenberg.py
+++ b/experimental/gutenberg/gutenberg.py
@@ -24,7 +24,6 @@ import operator
import time
import re
-from itertools import islice, izip
import logging
import random
import json
diff --git a/payment/stripelib.py b/payment/stripelib.py
index ae99e8b0..d66f2d77 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -4,6 +4,7 @@
import logging
from datetime import datetime, timedelta
from pytz import utc
+from itertools import islice
from django.conf import settings
@@ -17,6 +18,17 @@ import stripe
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
@@ -46,8 +58,8 @@ try:
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 = 'pk_0EajXPn195ZdF7Gt7pCxsqRhNN5BF'
- STRIPE_SK = 'sk_0EajIO4Dnh646KPIgLWGcO10f9qnH'
+ STRIPE_PK = TEST_STRIPE_PK
+ STRIPE_SK = TEST_STRIPE_SK
# set default stripe api_key to that of unglue.it
@@ -162,7 +174,6 @@ class StripeClient(object):
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)
@@ -208,8 +219,26 @@ class StripeClient(object):
def list_all_charges(self, count=None, offset=None, customer=None):
# https://stripe.com/docs/api?lang=python#list_charges
return stripe.Charge(api_key=self.api_key).all(count=count, offset=offset, customer=customer)
-
+
+ def list_events(self, **kwargs):
+ """a generator for events"""
+ # type=None, created=None, count=None, offset=0
+ #
+ kwargs2 = kwargs.copy()
+ kwargs2.setdefault('offset', 0)
+ kwargs2.setdefault('count', 100)
+
+ more_items = True
+ while more_items:
+ items = self.event.all(**kwargs2)['data']
+ for item in items:
+ yield item
+ if len(items):
+ kwargs2['offset'] += len(items)
+ else:
+ more_items = False
+
# can't test Transfer in test mode: "There are no transfers in test mode."
#pledge scenario
From efbe84b9fd606293ef5d9ff8f89fcb15b4da87d3 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Thu, 18 Oct 2012 11:16:18 -0700
Subject: [PATCH 03/33] a small bug fix -- sometimes instances don't have ids
also now list all instances when running python aws.py
---
sysadmin/aws.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/sysadmin/aws.py b/sysadmin/aws.py
index 3d2fe37f..9c51ebf1 100644
--- a/sysadmin/aws.py
+++ b/sysadmin/aws.py
@@ -89,8 +89,12 @@ def instance_metrics(instance):
for metric in metrics:
if 'InstanceId' in metric.dimensions:
- if instance.id in metric.dimensions['InstanceId']:
- my_metrics.append(metric)
+ # instance.id not guaranteed to be there
+ try:
+ if instance.id in metric.dimensions['InstanceId']:
+ my_metrics.append(metric)
+ except:
+ pass
return my_metrics
@@ -346,4 +350,5 @@ if __name__ == '__main__':
pprint (stats_for_instances(all_instances()))
web1 = instance('web1')
print instance_metrics(web1)
+ print all_images()
From b231011515166811327d5e19a3e7d8657a08da19 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Fri, 19 Oct 2012 18:11:46 -0700
Subject: [PATCH 04/33] handle in a generic fashion all Stripe listable objects
---
payment/stripelib.py | 76 ++++++++++++++++++++++++++++++++------------
1 file changed, 56 insertions(+), 20 deletions(-)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index d66f2d77..1fd09b5f 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -147,6 +147,12 @@ def card (number=TEST_CARDS[0][0], exp_month=1, exp_year=2020, cvc=None, name=No
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):
@@ -214,30 +220,59 @@ class StripeClient(object):
# 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 list_all_charges(self, count=None, offset=None, customer=None):
- # https://stripe.com/docs/api?lang=python#list_charges
- return stripe.Charge(api_key=self.api_key).all(count=count, offset=offset, customer=customer)
+ return ch
- def list_events(self, **kwargs):
- """a generator for events"""
+ 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'
- kwargs2 = kwargs.copy()
- kwargs2.setdefault('offset', 0)
- kwargs2.setdefault('count', 100)
-
- more_items = True
- while more_items:
- items = self.event.all(**kwargs2)['data']
- for item in items:
- yield item
- if len(items):
- kwargs2['offset'] += len(items)
+ 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:
- more_items = False
+ 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."
@@ -286,6 +321,7 @@ class StripeClient(object):
# * expired_card -- also not easily simulatable in test mode
+
class StripeErrorTest(TestCase):
"""Make sure the exceptions returned by stripe act as expected"""
From 6af74af6e05d81d1e5e8499b17f3d3a814cda1ae Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Mon, 22 Oct 2012 06:55:30 -0700
Subject: [PATCH 05/33] Added "from django.http import HttpResponse"
---
payment/stripelib.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 1fd09b5f..37301f6b 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -7,6 +7,7 @@ from pytz import utc
from itertools import islice
from django.conf import settings
+from django.http import HttpResponse
from regluit.payment.models import Account
from regluit.payment.parameters import PAYMENT_HOST_STRIPE
From 5f75a173103f0f7f6299791cb48cd82e3b1ac250 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Mon, 22 Oct 2012 07:50:52 -0700
Subject: [PATCH 06/33] First pass at parsing json + log webhook
---
payment/stripelib.py | 13 ++++++++++++-
payment/views.py | 7 ++-----
2 files changed, 14 insertions(+), 6 deletions(-)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 37301f6b..d198380a 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -2,6 +2,7 @@
# https://stripe.com/docs/api?lang=python#top
import logging
+import json
from datetime import datetime, timedelta
from pytz import utc
from itertools import islice
@@ -673,7 +674,17 @@ class Processor(baseprocessor.Processor):
self.currency = transaction.currency
self.amount = transaction.amount
def ProcessIPN(self, request):
- return HttpResponse("hello from Stripe IPN")
+ # retrieve the request's body and parse it as JSON in, e.g. Django
+ #event_json = json.loads(request.body)
+ # what places to put in db to log?
+ try:
+ event_json = json.loads(request.body)
+ logger.info("event_json: {0}".format(event_json))
+ except ValueError:
+ # not able to parse request.body -- throw a "Bad Request" error
+ return HttpResponse(status=400)
+ else:
+ return HttpResponse("hello from Stripe IPN: {0}".format(event_json))
def suite():
diff --git a/payment/views.py b/payment/views.py
index 35397d07..7feccf0d 100644
--- a/payment/views.py
+++ b/payment/views.py
@@ -301,14 +301,11 @@ def runTests(request):
@csrf_exempt
def handleIPN(request, module):
- # Handler for paypal IPN notifications
+ # Handler for IPN /webhook notifications
p = PaymentManager()
- p.processIPN(request, module)
-
logger.info(str(request.POST))
- return HttpResponse("ipn")
-
+ return p.processIPN(request, module)
def paymentcomplete(request):
# pick up all get and post parameters and display
From 86aae9f18b2953c34906c453469291549fbff53e Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Mon, 22 Oct 2012 08:25:47 -0700
Subject: [PATCH 07/33] Now ask Stripe for event info in ipn
---
payment/stripelib.py | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index d198380a..8d185c47 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -684,7 +684,19 @@ class Processor(baseprocessor.Processor):
# not able to parse request.body -- throw a "Bad Request" error
return HttpResponse(status=400)
else:
- return HttpResponse("hello from Stripe IPN: {0}".format(event_json))
+ # 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.info("Invalid Event ID: {0}".format(event_id))
+ return HttpResponse(status=400)
+ else:
+ event_type = event.get("type")
+ return HttpResponse("event_id: {0} event_type: {1}".format(event_id, event_type))
def suite():
From 8b2e1054e997eaef9456ebac985f0d2a6cfb815e Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Tue, 23 Oct 2012 09:29:47 -0700
Subject: [PATCH 08/33] Start to break down the various type of events we'll
need to handle
---
payment/stripelib.py | 23 +++++++++++++++++++++--
1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 8d185c47..1afdee06 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -675,8 +675,6 @@ class Processor(baseprocessor.Processor):
self.amount = transaction.amount
def ProcessIPN(self, request):
# retrieve the request's body and parse it as JSON in, e.g. Django
- #event_json = json.loads(request.body)
- # what places to put in db to log?
try:
event_json = json.loads(request.body)
logger.info("event_json: {0}".format(event_json))
@@ -696,6 +694,27 @@ class Processor(baseprocessor.Processor):
return HttpResponse(status=400)
else:
event_type = event.get("type")
+ # https://stripe.com/docs/api?lang=python#event_types -- type to delegate things
+ # parse out type as resource.action
+ # use signals?
+ try:
+ (resource, action) = re.match("([^\.]+)\.(.*)", event_type).groups()
+ except:
+ 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
+ pass
+ elif resource == 'customer':
+ # handle created, updated, deleted
+ pass
+ else:
+ pass
+
return HttpResponse("event_id: {0} event_type: {1}".format(event_id, event_type))
From 29438050d15f2128f6c61911e1df8b0ea97e60ad Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Tue, 23 Oct 2012 14:20:13 -0700
Subject: [PATCH 09/33] test of sending email to RY upon customer.created event
---
payment/stripelib.py | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 1afdee06..98ee677d 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -9,6 +9,7 @@ from itertools import islice
from django.conf import settings
from django.http import HttpResponse
+from django.core.mail import send_mail
from regluit.payment.models import Account
from regluit.payment.parameters import PAYMENT_HOST_STRIPE
@@ -710,8 +711,21 @@ class Processor(baseprocessor.Processor):
# we need to handle: succeeded, failed, refunded, disputed
pass
elif resource == 'customer':
- # handle created, updated, deleted
- pass
+ if 'action' == 'created':
+ # test application: email Raymond
+ # do we have a flag to indicate production vs non-production? -- or does it matter?
+ try:
+ ev_object = event.get("data").get("object")
+ except:
+ pass
+ else:
+ send_mail("Stripe Customer for {0} created".format(ev_object.get("description")),
+ "Stripe Customer email: {0}".format(ev_object.get("email")),
+ "notices@gluejar.com",
+ ["rdhyee@gluejar.com"])
+ # handle updated, deleted
+ else:
+ pass
else:
pass
From 9b981926d2811b9698ed8352bdb91d99678361f2 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Tue, 23 Oct 2012 14:33:19 -0700
Subject: [PATCH 10/33] More exception logging for webhooks handling
---
payment/stripelib.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 98ee677d..31dcf727 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -678,7 +678,6 @@ class Processor(baseprocessor.Processor):
# retrieve the request's body and parse it as JSON in, e.g. Django
try:
event_json = json.loads(request.body)
- logger.info("event_json: {0}".format(event_json))
except ValueError:
# not able to parse request.body -- throw a "Bad Request" error
return HttpResponse(status=400)
@@ -700,7 +699,8 @@ class Processor(baseprocessor.Processor):
# use signals?
try:
(resource, action) = re.match("([^\.]+)\.(.*)", event_type).groups()
- except:
+ except Exception, e:
+ logger.error("exception {0} -- had problem parsing resource, action out of {1}".format(e, event_type))
return HttpResponse(status=400)
if event_type == 'account.updated':
@@ -716,8 +716,8 @@ class Processor(baseprocessor.Processor):
# do we have a flag to indicate production vs non-production? -- or does it matter?
try:
ev_object = event.get("data").get("object")
- except:
- pass
+ except Exception, e:
+ logger.error(e)
else:
send_mail("Stripe Customer for {0} created".format(ev_object.get("description")),
"Stripe Customer email: {0}".format(ev_object.get("email")),
From b4b5437ff5f22b70c9bb9a3fb48e3d9fa30e37d4 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Tue, 23 Oct 2012 22:18:03 +0000
Subject: [PATCH 11/33] Now working -- can send email to RY for
customer.created
---
payment/stripelib.py | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 98ee677d..384ae9fa 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -6,6 +6,7 @@ import json
from datetime import datetime, timedelta
from pytz import utc
from itertools import islice
+import re
from django.conf import settings
from django.http import HttpResponse
@@ -678,9 +679,10 @@ class Processor(baseprocessor.Processor):
# retrieve the request's body and parse it as JSON in, e.g. Django
try:
event_json = json.loads(request.body)
- logger.info("event_json: {0}".format(event_json))
- except ValueError:
+ logger.debug("event_json: {0}".format(event_json))
+ 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
@@ -691,7 +693,7 @@ class Processor(baseprocessor.Processor):
try:
event = sc.event.retrieve(event_id)
except stripe.InvalidRequestError:
- logger.info("Invalid Event ID: {0}".format(event_id))
+ logger.warning("Invalid Event ID: {0}".format(event_id))
return HttpResponse(status=400)
else:
event_type = event.get("type")
@@ -700,7 +702,8 @@ class Processor(baseprocessor.Processor):
# use signals?
try:
(resource, action) = re.match("([^\.]+)\.(.*)", event_type).groups()
- except:
+ except Exception, e:
+ logger.warning("Parsing of event_type into resource, action failed: {0}".format(e))
return HttpResponse(status=400)
if event_type == 'account.updated':
@@ -711,18 +714,20 @@ class Processor(baseprocessor.Processor):
# we need to handle: succeeded, failed, refunded, disputed
pass
elif resource == 'customer':
- if 'action' == 'created':
+ if action == 'created':
# test application: email Raymond
# do we have a flag to indicate production vs non-production? -- or does it matter?
try:
ev_object = event.get("data").get("object")
- except:
- pass
+ except Exception, e:
+ logger.warning("attempt to retrieve event object failed: {0}".format(e))
+ return HttpResponse(status=400)
else:
- send_mail("Stripe Customer for {0} created".format(ev_object.get("description")),
+ 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
From 32b20556c2897b89dc7755ae1e68c3be42903306 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Tue, 30 Oct 2012 11:17:09 -0700
Subject: [PATCH 12/33] Skeleton for handling charge related webhooks
---
payment/stripelib.py | 37 ++++++++++++++++++++++++-------------
1 file changed, 24 insertions(+), 13 deletions(-)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 384ae9fa..7856cf29 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -706,32 +706,43 @@ class Processor(baseprocessor.Processor):
logger.warning("Parsing of event_type into resource, action failed: {0}".format(e))
return HttpResponse(status=400)
+ try:
+ ev_object = event.get("data").get("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
- pass
+ if action == 'succeeded':
+ logger.info("charge.succeeded webhook for {0}".format(ev_object.get("id")))
+ elif action == 'failed':
+ pass
+ elif action == 'refunded':
+ pass
+ elif action == 'disputed':
+ pass
+ else:
+ 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?
- try:
- ev_object = event.get("data").get("object")
- except Exception, e:
- logger.warning("attempt to retrieve event object failed: {0}".format(e))
- return HttpResponse(status=400)
- else:
- 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")))
+ # 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:
+ else: # other events
pass
return HttpResponse("event_id: {0} event_type: {1}".format(event_id, event_type))
From 5df6dea26ba17e7fc7acf1ffe2019e0d25af87a4 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Tue, 30 Oct 2012 20:01:41 +0000
Subject: [PATCH 13/33] Comment section stripelib.Processor.Execute that needs
to be modified in light of how we now let users update the CC on file
---
payment/stripelib.py | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 7856cf29..564f4126 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -623,8 +623,8 @@ class Processor(baseprocessor.Processor):
# look at transaction.preapproval_key
# 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
+ # BUGBUG: replace description with something more useful
+ # BUGBUG: we should charge transaction against the User's current account -- not a cus_ token....
if transaction.preapproval_key.startswith('cus_'):
try:
charge = sc.create_charge(transaction.amount, customer=transaction.preapproval_key, description="${0} for test / retain cc".format(transaction.amount))
@@ -719,7 +719,10 @@ class Processor(baseprocessor.Processor):
elif resource == 'charge':
# we need to handle: succeeded, failed, refunded, disputed
if action == 'succeeded':
+ from regluit.payment.signals import transaction_charged
logger.info("charge.succeeded webhook for {0}".format(ev_object.get("id")))
+ # figure out how to pull related transaction if any
+
elif action == 'failed':
pass
elif action == 'refunded':
From 30fd1332af463f62e7591b029912f4972d5c8520 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Wed, 31 Oct 2012 08:23:13 -0700
Subject: [PATCH 14/33] Expanding functionality of stripelib.Processor.Execute
-- better error handling
---
payment/models.py | 2 +-
payment/stripelib.py | 56 ++++++++++++++++++++++++++------------------
2 files changed, 34 insertions(+), 24 deletions(-)
diff --git a/payment/models.py b/payment/models.py
index f7544bd8..b9669c50 100644
--- a/payment/models.py
+++ b/payment/models.py
@@ -56,7 +56,7 @@ class Transaction(models.Model):
# whether a Preapproval has been approved or not
approved = models.NullBooleanField(null=True)
- # error message from a PayPal transaction
+ # error message from a transaction
error = models.CharField(max_length=256, null=True)
# IPN.reason_code
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 7856cf29..271d341c 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -14,7 +14,7 @@ from django.core.mail import send_mail
from regluit.payment.models import Account
from regluit.payment.parameters import PAYMENT_HOST_STRIPE
-from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, PAYMENT_TYPE_AUTHORIZATION, TRANSACTION_STATUS_CANCELED
+from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_ERROR, PAYMENT_TYPE_AUTHORIZATION, TRANSACTION_STATUS_CANCELED
from regluit.payment import baseprocessor
from regluit.utils.localdatetime import now, zuluformat
@@ -548,9 +548,6 @@ class Processor(baseprocessor.Processor):
user.profile.account.deactivate()
account.save()
return account
-
- class Pay(StripePaymentRequest, baseprocessor.Processor.Pay):
- pass
class Preapproval(StripePaymentRequest, baseprocessor.Processor.Preapproval):
@@ -615,33 +612,46 @@ class Processor(baseprocessor.Processor):
def __init__(self, transaction=None):
self.transaction = transaction
- # execute transaction
- assert transaction.host == PAYMENT_HOST_STRIPE
+ # 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 at transaction.preapproval_key
- # is it a customer or a token?
+ # look first for transaction.user.profile.account.account_id
+ try:
+ customer_id = transaction.user.profile.account.account_id
+ except:
+ customer_id = transaction.preapproval_key
- # 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_'):
+ if customer_id is not None:
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_'):
- try:
- charge = sc.create_charge(transaction.amount, card=transaction.preapproval_key, description="${0} for test / cc not retained".format(transaction.amount))
+ # 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="t.id:{0}\temail:{1}\tcus.id:{2}\tc.id:{3}\tamount:{4}".format(transaction.id,
+ transaction.user.email, customer_id, transaction.campaign.id,
+ transaction.amount) )
+
except stripe.StripeError as e:
+ # what to record in terms of errors?
+
+ transaction.status = TRANSACTION_STATUS_ERROR
+ transaction.error = e.message
+ transaction.save()
+
raise StripelibError(e.message, e)
- transaction.status = TRANSACTION_STATUS_COMPLETE
- transaction.pay_key = charge.id
- transaction.date_payment = now()
- transaction.save()
-
- self.charge = charge
+ else:
+ self.charge = charge
+
+ transaction.status = TRANSACTION_STATUS_COMPLETE
+ transaction.pay_key = charge.id
+ transaction.date_payment = now()
+ transaction.save()
+ else:
+ # nothing to charge
+ raise StripeLibError("No customer id available to charge tor transaction {0}".format(transaction.id), None)
+
def api(self):
return "Base Pay"
From 96b62aa163bcf40316005c2f5396be12cfdb9292 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Wed, 31 Oct 2012 11:08:46 -0700
Subject: [PATCH 15/33] basic skeleton for test based on loading fixture keep
the signals for create userprofile, wishlist, and credit from firing if
fixtures being loaded
---
.../fixtures/basic_campaign_test.json | 0
core/signals.py | 23 ++++++++++---------
core/tests.py | 4 ----
frontend/tests.py | 22 +++++++++++++++++-
payment/signals.py | 18 ++++++++-------
payment/tests.py | 3 ---
sysadmin/load_test_fixture.sh | 2 +-
7 files changed, 44 insertions(+), 28 deletions(-)
rename {test => core}/fixtures/basic_campaign_test.json (100%)
diff --git a/test/fixtures/basic_campaign_test.json b/core/fixtures/basic_campaign_test.json
similarity index 100%
rename from test/fixtures/basic_campaign_test.json
rename to core/fixtures/basic_campaign_test.json
diff --git a/core/signals.py b/core/signals.py
index afebdbc1..a72d871b 100644
--- a/core/signals.py
+++ b/core/signals.py
@@ -31,20 +31,21 @@ def facebook_extra_values(sender, user, response, details, **kwargs):
pre_update.connect(facebook_extra_values, sender=FacebookBackend)
-
# create Wishlist and UserProfile to associate with User
def create_user_objects(sender, created, instance, **kwargs):
# use get_model to avoid circular import problem with models
- try:
- Wishlist = get_model('core', 'Wishlist')
- UserProfile = get_model('core', 'UserProfile')
- if created:
- Wishlist.objects.create(user=instance)
- UserProfile.objects.create(user=instance)
- except DatabaseError:
- # this can happen when creating superuser during syncdb since the
- # core_wishlist table doesn't exist yet
- return
+ # don't create Wishlist or UserProfile if we are loading fixtures http://stackoverflow.com/a/3500009/7782
+ if not kwargs.get('raw', False):
+ try:
+ Wishlist = get_model('core', 'Wishlist')
+ UserProfile = get_model('core', 'UserProfile')
+ if created:
+ Wishlist.objects.create(user=instance)
+ UserProfile.objects.create(user=instance)
+ except DatabaseError:
+ # this can happen when creating superuser during syncdb since the
+ # core_wishlist table doesn't exist yet
+ return
post_save.connect(create_user_objects, sender=User)
diff --git a/core/tests.py b/core/tests.py
index 487e4d38..ef71e897 100755
--- a/core/tests.py
+++ b/core/tests.py
@@ -326,9 +326,6 @@ class BookLoaderTests(TestCase):
ebook = bookloader.load_gutenberg_edition(title, gutenberg_etext_id, ol_work_id, seed_isbn, epub_url, format, license, lang, publication_date)
self.assertEqual(ebook.url, epub_url)
-
-
-
class SearchTests(TestCase):
@@ -380,7 +377,6 @@ class CampaignTests(TestCase):
self.assertEqual(c.license_url, 'http://creativecommons.org/licenses/by-nc/3.0/')
self.assertEqual(c.license_badge, 'https://i.creativecommons.org/l/by-nc/3.0/88x31.png')
-
def test_campaign_status(self):
# need a user to associate with a transaction
diff --git a/frontend/tests.py b/frontend/tests.py
index 0eb24d32..d71f23c2 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -173,4 +173,24 @@ class PledgingUiTests(TestCase):
def tearDown(self):
pass
-
\ No newline at end of file
+
+class UnifiedCampaignTests(TestCase):
+ fixtures=['basic_campaign_test.json']
+ def test_setup(self):
+ # testing basics: are there 3 users?
+ from django.contrib.auth.models import User
+ self.assertEqual(User.objects.count(), 3)
+ # make sure we know the passwords for the users
+ #RaymondYee / raymond.yee@gmail.com / Test_Password_
+ #hmelville / rdhyee@yahoo.com / gofish!
+ #dataunbound / raymond.yee@dataunbound.com / numbers_unbound
+ self.client = Client()
+ self.assertTrue(self.client.login(username="RaymondYee", password="Test_Password_"))
+ self.assertTrue(self.client.login(username="hmelville", password="gofish!"))
+ self.assertTrue(self.client.login(username="dataunbound", password="numbers_unbound"))
+
+
+ def test_relaunch(self):
+ # how much of test.campaigntest.test_relaunch can be done here?
+ pass
+
diff --git a/payment/signals.py b/payment/signals.py
index 5117fd5c..3b247ac3 100644
--- a/payment/signals.py
+++ b/payment/signals.py
@@ -14,14 +14,16 @@ from django.contrib.auth.models import User
# create Credit to associate with User
def create_user_objects(sender, created, instance, **kwargs):
# use get_model to avoid circular import problem with models
- try:
- Credit = get_model('payment', 'Credit')
- if created:
- Credit.objects.create(user=instance)
- except DatabaseError:
- # this can happen when creating superuser during syncdb since the
- # core_wishlist table doesn't exist yet
- return
+ # don't create Credit if we are loading fixtures http://stackoverflow.com/a/3500009/7782
+ if not kwargs.get('raw', False):
+ try:
+ Credit = get_model('payment', 'Credit')
+ if created:
+ Credit.objects.create(user=instance)
+ except DatabaseError:
+ # this can happen when creating superuser during syncdb since the
+ # core_wishlist table doesn't exist yet
+ return
post_save.connect(create_user_objects, sender=User)
diff --git a/payment/tests.py b/payment/tests.py
index 56614ea4..439de14c 100644
--- a/payment/tests.py
+++ b/payment/tests.py
@@ -323,8 +323,6 @@ class CreditTest(TestCase):
self.assertEqual(self.user1.credit.balance, 0)
self.assertEqual(self.user2.credit.balance, 50)
-
-
class TransactionTest(TestCase):
def setUp(self):
"""
@@ -387,7 +385,6 @@ class BasicGuiTest(TestCase):
def tearDown(self):
self.selenium.quit()
-
class AccountTest(TestCase):
def setUp(self):
diff --git a/sysadmin/load_test_fixture.sh b/sysadmin/load_test_fixture.sh
index def507d8..e305b6bf 100755
--- a/sysadmin/load_test_fixture.sh
+++ b/sysadmin/load_test_fixture.sh
@@ -3,5 +3,5 @@
django-admin.py syncdb --migrate <<'EOF'
no
EOF
-django-admin.py loaddata ../test/fixtures/basic_campaign_test.json
+django-admin.py loaddata ../core/fixtures/basic_campaign_test.json
From 14cd6209bf940b2b36d072bb0a88f280a5e4bf8a Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Wed, 31 Oct 2012 11:57:39 -0700
Subject: [PATCH 16/33] continuing on UnifiedCampaignTests
---
frontend/tests.py | 27 ++++++++++++++++++++++++---
1 file changed, 24 insertions(+), 3 deletions(-)
diff --git a/frontend/tests.py b/frontend/tests.py
index d71f23c2..668c7af9 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -179,6 +179,7 @@ class UnifiedCampaignTests(TestCase):
def test_setup(self):
# testing basics: are there 3 users?
from django.contrib.auth.models import User
+
self.assertEqual(User.objects.count(), 3)
# make sure we know the passwords for the users
#RaymondYee / raymond.yee@gmail.com / Test_Password_
@@ -188,9 +189,29 @@ class UnifiedCampaignTests(TestCase):
self.assertTrue(self.client.login(username="RaymondYee", password="Test_Password_"))
self.assertTrue(self.client.login(username="hmelville", password="gofish!"))
self.assertTrue(self.client.login(username="dataunbound", password="numbers_unbound"))
-
- def test_relaunch(self):
+ # how many works and campaigns?
+ self.assertEqual(Work.objects.count(), 3)
+ self.assertEqual(Campaign.objects.count(), 2)
+
+
+ def test_pledge1(self):
# how much of test.campaigntest.test_relaunch can be done here?
- pass
+ self.assertTrue(self.client.login(username="RaymondYee", password="Test_Password_"))
+
+ # Pro Web 2.0 Mashups
+ self.work = Work.objects.get(id=1)
+ r = self.client.get("/work/%s/" % (self.work.id))
+
+ r = self.client.get("/work/%s/" % self.work.id)
+ self.assertEqual(r.status_code, 200)
+
+ # go to pledge page
+ r = self.client.get("/pledge/%s" % self.work.id, data={}, follow=True)
+ self.assertEqual(r.status_code, 200)
+
+ # submit to pledge page
+ r = self.client.post("/pledge/%s" % self.work.id, data={'preapproval_amount':'10',
+ 'premium_id':'150'}, follow=True)
+ self.assertEqual(r.status_code, 200)
From 9826951d36fa6c499b9df2d17fec66dc48cbbde2 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Thu, 1 Nov 2012 14:06:11 -0700
Subject: [PATCH 17/33] Continuing to build out UnifiedCampaignTests -- now
includes scenario where a pledge is made and charged -- and the corresponding
Stripe events are polled and inspected
---
frontend/tests.py | 54 +++++++++++++++++++++++++++++++++++++++++---
payment/stripelib.py | 1 -
2 files changed, 51 insertions(+), 4 deletions(-)
diff --git a/frontend/tests.py b/frontend/tests.py
index 668c7af9..1cb0632f 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -7,10 +7,14 @@ from django.core.urlresolvers import reverse
from django.conf import settings
from regluit.core.models import Work, Campaign, RightsHolder, Claim
+from regluit.payment.models import Transaction
+from regluit.payment.manager import PaymentManager
+from regluit.payment.stripelib import StripeClient, TEST_CARDS, card
from decimal import Decimal as D
from regluit.utils.localdatetime import now
from datetime import timedelta
+import time
class WishlistTests(TestCase):
@@ -178,7 +182,6 @@ class UnifiedCampaignTests(TestCase):
fixtures=['basic_campaign_test.json']
def test_setup(self):
# testing basics: are there 3 users?
- from django.contrib.auth.models import User
self.assertEqual(User.objects.count(), 3)
# make sure we know the passwords for the users
@@ -211,7 +214,52 @@ class UnifiedCampaignTests(TestCase):
self.assertEqual(r.status_code, 200)
# submit to pledge page
- r = self.client.post("/pledge/%s" % self.work.id, data={'preapproval_amount':'10',
+ r = self.client.post("/pledge/%s/" % self.work.id, data={'preapproval_amount':'10',
'premium_id':'150'}, follow=True)
- self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.status_code, 200)
+
+ # Should now be on the fund page
+ pledge_fund_path = r.request.get('PATH_INFO')
+ self.assertTrue(pledge_fund_path.startswith('/pledge/fund'))
+ # pull out the transaction info
+ t_id = int(pledge_fund_path.replace('/pledge/fund/',''))
+
+ # r.content holds the page content
+ # create a stripe token to submit to form
+
+ # track start time and end time of these stipe interactions so that we can limit the window of Events to look for
+ time0 = time.time()
+
+ sc = StripeClient()
+ card1 = card(number=TEST_CARDS[0][0], exp_month=1, exp_year='2020', cvc='123', name='Raymond Yee',
+ address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
+ stripe_token = sc.create_token(card=card1)
+ r = self.client.post(pledge_fund_path, data={'stripe_token':stripe_token.id}, follow=True)
+
+ # where are we now?
+ self.assertEqual(r.request.get('PATH_INFO'), '/pledge/complete/')
+ self.assertEqual(r.status_code, 200)
+
+ # dig up the transaction and charge it
+ pm = PaymentManager()
+ transaction = Transaction.objects.get(id=t_id)
+ self.assertTrue(pm.execute_transaction(transaction, ()))
+
+ time1 = time.time()
+
+ # retrieve events from this period -- need to pass in ints for event creation times
+ events = list(sc._all_objs('Event', created={'gte':int(time0), 'lte':int(time1+1.0)}))
+
+ # expect to have 2 events (there is a possibility that someone else could be running tests on this stripe account at the same time)
+ # events returned sorted in reverse chronological order.
+
+ self.assertEqual(len(events), 2)
+ self.assertEqual(events[0].type, 'charge.succeeded')
+ self.assertEqual(events[1].type, 'customer.created')
+
+
+
+
+
+
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 6655fdd4..9c03afab 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -689,7 +689,6 @@ class Processor(baseprocessor.Processor):
# retrieve the request's body and parse it as JSON in, e.g. Django
try:
event_json = json.loads(request.body)
- logger.debug("event_json: {0}".format(event_json))
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))
From f6bf774f2ce3650ab4e6e49490a16161de5de01a Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Fri, 2 Nov 2012 14:46:49 -0700
Subject: [PATCH 18/33] Test sending junk to webhook handler Simulate sending
events to the ipnhandler and check to see how many emails generated in test
---
frontend/tests.py | 29 +++++++++++++++++++++++++++++
payment/stripelib.py | 1 +
2 files changed, 30 insertions(+)
diff --git a/frontend/tests.py b/frontend/tests.py
index 1cb0632f..7bb1328f 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -5,6 +5,8 @@ from django.test.client import Client
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.conf import settings
+from django.core import mail
+
from regluit.core.models import Work, Campaign, RightsHolder, Claim
from regluit.payment.models import Transaction
@@ -15,6 +17,7 @@ from decimal import Decimal as D
from regluit.utils.localdatetime import now
from datetime import timedelta
import time
+import json
class WishlistTests(TestCase):
@@ -197,6 +200,16 @@ class UnifiedCampaignTests(TestCase):
self.assertEqual(Work.objects.count(), 3)
self.assertEqual(Campaign.objects.count(), 2)
+ def test_junk_webhook(self):
+ """send in junk json and then an event that doesn't exist"""
+ # non-json
+ ipn_url = reverse("HandleIPN", args=('stripelib',))
+ r = self.client.post(ipn_url, data="X", content_type="application/json; charset=utf-8")
+ self.assertEqual(r.status_code, 400)
+ # junk event_id
+ r = self.client.post(ipn_url, data='{"id": "evt_XXXXXXXXX"}', content_type="application/json; charset=utf-8")
+ self.assertEqual(r.status_code, 400)
+
def test_pledge1(self):
# how much of test.campaigntest.test_relaunch can be done here?
@@ -257,6 +270,22 @@ class UnifiedCampaignTests(TestCase):
self.assertEqual(events[0].type, 'charge.succeeded')
self.assertEqual(events[1].type, 'customer.created')
+ # now feed each of the events to the IPN processor.
+ ipn_url = reverse("HandleIPN", args=('stripelib',))
+
+ for (i, event) in enumerate(events):
+ r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
+ self.assertEqual(r.status_code, 200)
+
+ # confirm that an email was sent out for the customer.created event
+
+ # Test that one message has been sent.
+ # for initial pledging, for successful campaign, for Customer.created event
+ self.assertEqual(len(mail.outbox), 3)
+
+
+
+
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 9c03afab..495eca02 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -688,6 +688,7 @@ class Processor(baseprocessor.Processor):
def ProcessIPN(self, request):
# retrieve the request's body and parse it as JSON in, e.g. Django
try:
+ logger.info("request.body: {0}".format(request.body))
event_json = json.loads(request.body)
except ValueError, e:
# not able to parse request.body -- throw a "Bad Request" error
From 7ea5ac7ca621d20311579c09d91765be9f191339 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Mon, 5 Nov 2012 10:44:24 -0800
Subject: [PATCH 19/33] 1) Now fire the transaction_charged signal in the
Stripe webhook handling 2) add WebTest to requirements 3) have a check on
possible Stripe events
---
payment/manager.py | 2 --
payment/stripelib.py | 59 +++++++++++++++++++++++++++++---------
requirements_versioned.pip | 2 ++
3 files changed, 48 insertions(+), 15 deletions(-)
diff --git a/payment/manager.py b/payment/manager.py
index faf5e64f..4836cde3 100644
--- a/payment/manager.py
+++ b/payment/manager.py
@@ -482,8 +482,6 @@ class PaymentManager( object ):
if p.success() and not p.error():
transaction.pay_key = p.key()
transaction.save()
- logger.info("execute_transaction Success")
- transaction_charged.send(sender=self, transaction=transaction)
return True
else:
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 495eca02..53522a7f 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -12,7 +12,7 @@ from django.conf import settings
from django.http import HttpResponse
from django.core.mail import send_mail
-from regluit.payment.models import Account
+from regluit.payment.models import Account, Transaction
from regluit.payment.parameters import PAYMENT_HOST_STRIPE
from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_ERROR, PAYMENT_TYPE_AUTHORIZATION, TRANSACTION_STATUS_CANCELED
from regluit.payment import baseprocessor
@@ -20,6 +20,20 @@ from regluit.utils.localdatetime import now, zuluformat
import stripe
+# as of 2012.11.05
+STRIPE_EVENT_TYPES = ['account.updated', 'account.application.deauthorized',
+ 'charge.succeeded', 'charge.failed', 'charge.refunded', 'charge.disputed',
+ 'customer.created', 'customer.updated', 'customer.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.updated', 'coupon.deleted',
+ 'transfer.created', 'transfer.updated', 'transfer.failed',
+ 'ping']
+
logger = logging.getLogger(__name__)
# http://stackoverflow.com/questions/2348317/how-to-write-a-pager-for-python-iterators/2350904#2350904
@@ -461,8 +475,7 @@ class StripeErrorTest(TestCase):
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
@@ -628,10 +641,11 @@ class Processor(baseprocessor.Processor):
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="t.id:{0}\temail:{1}\tcus.id:{2}\tc.id:{3}\tamount:{4}".format(transaction.id,
- transaction.user.email, customer_id, transaction.campaign.id,
- transaction.amount) )
-
+ description=json.dumps({"t.id":transaction.id,
+ "email":transaction.user.email,
+ "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?
@@ -685,10 +699,10 @@ class Processor(baseprocessor.Processor):
# 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:
- logger.info("request.body: {0}".format(request.body))
event_json = json.loads(request.body)
except ValueError, e:
# not able to parse request.body -- throw a "Bad Request" error
@@ -707,17 +721,21 @@ class Processor(baseprocessor.Processor):
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
- # use signals?
+
try:
- (resource, action) = re.match("([^\.]+)\.(.*)", event_type).groups()
+ (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.get("data").get("object")
+ ev_object = event.data.object
except Exception, e:
logger.warning("attempt to retrieve event object failed: {0}".format(e))
return HttpResponse(status=400)
@@ -731,8 +749,22 @@ class Processor(baseprocessor.Processor):
if action == 'succeeded':
from regluit.payment.signals import transaction_charged
logger.info("charge.succeeded webhook for {0}".format(ev_object.get("id")))
- # figure out how to pull related transaction if any
-
+ # 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))
+ # now -- should fire off transaction_charged here -- if so we need to move it from ?
+ transaction_charged.send(sender=self, transaction=transaction)
+ except Exception, e:
+ logger.warning(e)
+
elif action == 'failed':
pass
elif action == 'refunded':
@@ -740,6 +772,7 @@ class Processor(baseprocessor.Processor):
elif action == 'disputed':
pass
else:
+ # unexpected
pass
elif resource == 'customer':
if action == 'created':
diff --git a/requirements_versioned.pip b/requirements_versioned.pip
index 5b9221c2..54b75260 100644
--- a/requirements_versioned.pip
+++ b/requirements_versioned.pip
@@ -4,6 +4,8 @@ MySQL-python==1.2.3
Pillow==1.7.7
Pyzotero==0.9.51
South==0.7.6
+WebOb==1.2.3
+WebTest==1.4.0
amqplib==1.0.2
anyjson==0.3.3
billiard==2.7.3.12
From 793d984ba3cd94e88e046f502be3820e31faf9cb Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Tue, 6 Nov 2012 11:22:25 -0800
Subject: [PATCH 20/33] Implemented basic transaction_failed signal and notices
-- THEY STILL NEED WORK Tests handle situation of transaction_failed too
---
core/signals.py | 18 ++++++++-
frontend/tests.py | 93 ++++++++++++++++++++++++++++++++++----------
payment/signals.py | 4 ++
payment/stripelib.py | 20 ++++++++--
4 files changed, 110 insertions(+), 25 deletions(-)
diff --git a/core/signals.py b/core/signals.py
index a72d871b..4699732f 100644
--- a/core/signals.py
+++ b/core/signals.py
@@ -14,7 +14,7 @@ from social_auth.signals import pre_update
from social_auth.backends.facebook import FacebookBackend
from tastypie.models import create_api_key
-from regluit.payment.signals import transaction_charged, pledge_modified, pledge_created
+from regluit.payment.signals import transaction_charged, transaction_failed, pledge_modified, pledge_created
import registration.signals
import django.dispatch
@@ -94,6 +94,7 @@ def create_notice_types(app, created_models, verbosity, **kwargs):
notification.create_notice_type("pledge_you_have_pledged", _("Thanks For Your Pledge!"), _("Your ungluing pledge has been entered."))
notification.create_notice_type("pledge_status_change", _("Your Pledge Has Been Modified"), _("Your ungluing pledge has been modified."))
notification.create_notice_type("pledge_charged", _("Your Pledge has been Executed"), _("You have contributed to a successful ungluing campaign."))
+ notification.create_notice_type("pledge_failed", _("Unable to charge your credit card"), _("A charge to your credit card did not go through."))
notification.create_notice_type("rights_holder_created", _("Agreement Accepted"), _("You have become a verified Unglue.it rights holder."))
notification.create_notice_type("rights_holder_claim_approved", _("Claim Accepted"), _("A claim you've entered has been accepted."))
notification.create_notice_type("wishlist_unsuccessful_amazon", _("Campaign shut down"), _("An ungluing campaign that you supported had to be shut down due to an Amazon Payments policy change."))
@@ -171,6 +172,21 @@ def handle_transaction_charged(sender,transaction=None, **kwargs):
transaction_charged.connect(handle_transaction_charged)
+# dealing with failed transactions
+
+def handle_transaction_failed(sender,transaction=None, **kwargs):
+ if transaction==None:
+ return
+ notification.queue([transaction.user], "pledge_failed", {
+ 'site':Site.objects.get_current(),
+ 'transaction':transaction
+ }, True)
+ from regluit.core.tasks import emit_notifications
+ emit_notifications.delay()
+
+transaction_failed.connect(handle_transaction_failed)
+
+
def handle_pledge_modified(sender, transaction=None, up_or_down=None, **kwargs):
# we need to know if pledges were modified up or down because Amazon handles the
# transactions in different ways, resulting in different user-visible behavior;
diff --git a/frontend/tests.py b/frontend/tests.py
index 7bb1328f..30f7e6be 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -11,7 +11,7 @@ from django.core import mail
from regluit.core.models import Work, Campaign, RightsHolder, Claim
from regluit.payment.models import Transaction
from regluit.payment.manager import PaymentManager
-from regluit.payment.stripelib import StripeClient, TEST_CARDS, card
+from regluit.payment.stripelib import StripeClient, TEST_CARDS, ERROR_TESTING, card
from decimal import Decimal as D
from regluit.utils.localdatetime import now
@@ -209,14 +209,14 @@ class UnifiedCampaignTests(TestCase):
# junk event_id
r = self.client.post(ipn_url, data='{"id": "evt_XXXXXXXXX"}', content_type="application/json; charset=utf-8")
self.assertEqual(r.status_code, 400)
-
- def test_pledge1(self):
+ def pledge_to_work_with_cc(self, username, password, work_id, card, preapproval_amount='10', premium_id='150'):
+
# how much of test.campaigntest.test_relaunch can be done here?
- self.assertTrue(self.client.login(username="RaymondYee", password="Test_Password_"))
+ self.assertTrue(self.client.login(username=username, password=password))
# Pro Web 2.0 Mashups
- self.work = Work.objects.get(id=1)
+ self.work = Work.objects.get(id=work_id)
r = self.client.get("/work/%s/" % (self.work.id))
r = self.client.get("/work/%s/" % self.work.id)
@@ -227,8 +227,8 @@ class UnifiedCampaignTests(TestCase):
self.assertEqual(r.status_code, 200)
# submit to pledge page
- r = self.client.post("/pledge/%s/" % self.work.id, data={'preapproval_amount':'10',
- 'premium_id':'150'}, follow=True)
+ r = self.client.post("/pledge/%s/" % self.work.id, data={'preapproval_amount': preapproval_amount,
+ 'premium_id':premium_id}, follow=True)
self.assertEqual(r.status_code, 200)
# Should now be on the fund page
@@ -244,9 +244,7 @@ class UnifiedCampaignTests(TestCase):
time0 = time.time()
sc = StripeClient()
- card1 = card(number=TEST_CARDS[0][0], exp_month=1, exp_year='2020', cvc='123', name='Raymond Yee',
- address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
- stripe_token = sc.create_token(card=card1)
+ stripe_token = sc.create_token(card=card)
r = self.client.post(pledge_fund_path, data={'stripe_token':stripe_token.id}, follow=True)
# where are we now?
@@ -256,12 +254,32 @@ class UnifiedCampaignTests(TestCase):
# dig up the transaction and charge it
pm = PaymentManager()
transaction = Transaction.objects.get(id=t_id)
- self.assertTrue(pm.execute_transaction(transaction, ()))
+
+ # catch any exception and pass it along
+ try:
+ self.assertTrue(pm.execute_transaction(transaction, ()))
+ except Exception, charge_exception:
+ pass
+ else:
+ charge_exception = None
time1 = time.time()
# retrieve events from this period -- need to pass in ints for event creation times
events = list(sc._all_objs('Event', created={'gte':int(time0), 'lte':int(time1+1.0)}))
+
+ return (events, charge_exception)
+
+ def good_cc_scenario(self):
+ # how much of test.campaigntest.test_relaunch can be done here?
+
+ card1 = card(number=TEST_CARDS[0][0], exp_month=1, exp_year='2020', cvc='123', name='Raymond Yee',
+ address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
+
+ (events, charge_exception) = self.pledge_to_work_with_cc(username="RaymondYee", password="Test_Password_", work_id=1, card=card1,
+ preapproval_amount='10', premium_id='150')
+
+ self.assertEqual(charge_exception, None)
# expect to have 2 events (there is a possibility that someone else could be running tests on this stripe account at the same time)
# events returned sorted in reverse chronological order.
@@ -276,19 +294,52 @@ class UnifiedCampaignTests(TestCase):
for (i, event) in enumerate(events):
r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
self.assertEqual(r.status_code, 200)
+
+ def bad_cc_scenario(self):
+ """Goal of this scenario: enter a CC that will cause a charge.failed event, have user repledge succesfully"""
+
+ card1 = card(number=ERROR_TESTING['BAD_ATTACHED_CARD'][0])
+
+ (events, charge_exception) = self.pledge_to_work_with_cc(username="dataunbound", password="numbers_unbound", work_id=2, card=card1,
+ preapproval_amount='10', premium_id='150')
+
+ # we should have an exception when the charge was attempted
+ self.assertTrue(charge_exception is not None)
+
+ # expect to have 2 events (there is a possibility that someone else could be running tests on this stripe account at the same time)
+ # events returned sorted in reverse chronological order.
+
+ self.assertEqual(len(events), 2)
+ self.assertEqual(events[0].type, 'charge.failed')
+ self.assertEqual(events[1].type, 'customer.created')
+
+ # now feed each of the events to the IPN processor.
+ ipn_url = reverse("HandleIPN", args=('stripelib',))
+
+ for (i, event) in enumerate(events):
+ r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
+ self.assertEqual(r.status_code, 200)
- # confirm that an email was sent out for the customer.created event
-
- # Test that one message has been sent.
- # for initial pledging, for successful campaign, for Customer.created event
- self.assertEqual(len(mail.outbox), 3)
-
+ def test_good_bad_cc_scenarios(self):
+ self.good_cc_scenario()
+ self.bad_cc_scenario()
+
+
+ # look at emails generated through these scenarios
+ #print len(mail.outbox)
+ #for (i, m) in enumerate(mail.outbox):
+ # print i, m.subject
-
-
-
-
+#0 [localhost:8000] Thank you for supporting Pro Web 2.0 Mashups at Unglue.it!
+#1 [localhost:8000] Thanks to you, the campaign for Pro Web 2.0 Mashups has succeeded!
+#2 Stripe Customer (id cus_0gf9OjXHDhBwjw; description: RaymondYee) created
+#3 [localhost:8000] Thank you for supporting Moby Dick at Unglue.it!
+#4 [localhost:8000] Someone new has wished for your work at Unglue.it
+#5 [localhost:8000] Thanks to you, the campaign for Moby Dick has succeeded! However, your credit card charge failed.
+#6 Stripe Customer (id cus_0gf93tJp036kmG; description: dataunbound) created
+
+ self.assertEqual(len(mail.outbox), 7)
diff --git a/payment/signals.py b/payment/signals.py
index 3b247ac3..8043393f 100644
--- a/payment/signals.py
+++ b/payment/signals.py
@@ -2,6 +2,10 @@ from notification import models as notification
from django.dispatch import Signal
transaction_charged = Signal(providing_args=["transaction"])
+transaction_failed = Signal(providing_args=["transaction"])
+transaction_refunded = Signal(providing_args=["transaction"])
+transaction_disputed = Signal(providing_args=["transaction"])
+
pledge_created = Signal(providing_args=["transaction"])
pledge_modified = Signal(providing_args=["transaction", "up_or_down"])
credit_balance_added = Signal(providing_args=["amount"])
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 53522a7f..4302ed4c 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -16,6 +16,8 @@ from regluit.payment.models import Account, Transaction
from regluit.payment.parameters import PAYMENT_HOST_STRIPE
from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_ERROR, PAYMENT_TYPE_AUTHORIZATION, TRANSACTION_STATUS_CANCELED
from regluit.payment import baseprocessor
+from regluit.payment.signals import transaction_charged, transaction_failed
+
from regluit.utils.localdatetime import now, zuluformat
import stripe
@@ -391,7 +393,7 @@ class StripeErrorTest(TestCase):
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.")
+ 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
@@ -747,7 +749,6 @@ class Processor(baseprocessor.Processor):
elif resource == 'charge':
# we need to handle: succeeded, failed, refunded, disputed
if action == 'succeeded':
- from regluit.payment.signals import transaction_charged
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
@@ -766,7 +767,20 @@ class Processor(baseprocessor.Processor):
logger.warning(e)
elif action == 'failed':
- pass
+ 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))
+ # now -- should fire off transaction_charged here -- if so we need to move it from ?
+ transaction_failed.send(sender=self, transaction=transaction)
+ except Exception, e:
+ logger.warning(e)
elif action == 'refunded':
pass
elif action == 'disputed':
From a90f1331669f72e242f4510c51af5aee1281eb62 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Mon, 12 Nov 2012 11:22:20 -0800
Subject: [PATCH 21/33] First pass at writing a signal handler to recharge
transaction if account updated
---
payment/models.py | 31 ++++++++++++++++++++++++++-----
payment/parameters.py | 2 ++
settings/common.py | 3 +++
3 files changed, 31 insertions(+), 5 deletions(-)
diff --git a/payment/models.py b/payment/models.py
index b9669c50..9795d00d 100644
--- a/payment/models.py
+++ b/payment/models.py
@@ -1,12 +1,17 @@
from django.db import models
from django.contrib.auth.models import User
from django.conf import settings
+from django.db.models import Q
+import regluit.payment.manager
from regluit.payment.parameters import *
from regluit.payment.signals import credit_balance_added, pledge_created
from regluit.utils.localdatetime import now
+
+from django.db.models.signals import post_save, post_delete
+
from decimal import Decimal
-from datetime import timedelta
+import datetime
import uuid
import urllib
import logging
@@ -136,7 +141,7 @@ class Transaction(models.Model):
self.approved=True
now_val = now()
self.date_authorized = now_val
- self.date_expired = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD )
+ self.date_expired = now_val + datetime.timedelta( days=settings.PREAPPROVAL_PERIOD )
self.save()
pledge_created.send(sender=self, transaction=self)
@@ -333,9 +338,6 @@ class Account(models.Model):
self.date_deactivated = now()
self.save()
-from django.db.models.signals import post_save, post_delete
-import regluit.payment.manager
-
# handle any save, updates to a payment.Transaction
def handle_transaction_change(sender, instance, created, **kwargs):
@@ -355,3 +357,22 @@ def handle_transaction_delete(sender, instance, **kwargs):
post_save.connect(handle_transaction_change,sender=Transaction)
post_delete.connect(handle_transaction_delete,sender=Transaction)
+def recharge_failed_transactions(sender, created, instance, **kwargs):
+ """When a new Account is saved, check whether this is the new active account for a user. If so, recharge any
+ outstanding failed transactions
+ """
+
+ transactions_to_recharge = instance.user.transaction_set.filter((Q(status=TRANSACTION_STATUS_FAILED) | Q(status=TRANSACTION_STATUS_ERROR)) & Q(campaign__status='UNSUCCESSFUL')).all()
+
+ if transactions_to_recharge:
+ pm = manager.PaymentManager()
+ for transaction in transactions_to_recharge:
+ # check whether we are still within the window to recharge
+ if (now() - transaction.campaign.deadline) < datetime.timedelta(settings.RECHARGE_WINDOW):
+ pm.execute_transaction(transaction)
+
+post_save.connect(recharge_failed_transactions, sender=Account)
+
+# handle recharging failed transactions
+
+
diff --git a/payment/parameters.py b/payment/parameters.py
index 027bb7f6..fac4cedf 100644
--- a/payment/parameters.py
+++ b/payment/parameters.py
@@ -48,3 +48,5 @@ TRANSACTION_STATUS_REFUNDED = 'Refunded'
# The transaction was refused/denied
TRANSACTION_STATUS_FAILED = 'Failed'
+# Transaction written off -- unable to successfully be charged after campaign succeeded
+TRANSACTION_STATUS_WRITTEN_OFF = 'Written-Off'
diff --git a/settings/common.py b/settings/common.py
index aa3ac0e1..43dca13e 100644
--- a/settings/common.py
+++ b/settings/common.py
@@ -226,6 +226,9 @@ GLUEJAR_COMMISSION = 0.06
PREAPPROVAL_PERIOD = 365 # days to ask for in a preapproval
PREAPPROVAL_PERIOD_AFTER_CAMPAIGN = 90 # if we ask for preapproval time after a campaign deadline
+# How many days we will try to collect on failed transactions until they are written off
+RECHARGE_WINDOW = 7
+
GOODREADS_API_KEY = ""
GOODREADS_API_SECRET = ""
From f2bb53f132555bd2ec7adff0245f26572f1136c3 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Mon, 12 Nov 2012 12:48:03 -0800
Subject: [PATCH 22/33] recharge signal being fired now
---
frontend/tests.py | 24 +++++++++++++++++++++---
payment/models.py | 19 +++++++++++++------
2 files changed, 34 insertions(+), 9 deletions(-)
diff --git a/frontend/tests.py b/frontend/tests.py
index 30f7e6be..672fe4c0 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -320,11 +320,26 @@ class UnifiedCampaignTests(TestCase):
r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
self.assertEqual(r.status_code, 200)
+ def recharge_with_new_card(self):
+
+ # mark campaign as SUCCESSFUL -- campaign for work 2
+ c = Work.objects.get(id=2).last_campaign()
+ c.status = 'SUCCESSFUL'
+ c.save()
+
+ # set up a good card
+ card1 = card(number=TEST_CARDS[0][0], exp_month=1, exp_year='2020', cvc='123', name='dataunbound',
+ address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
+ sc = StripeClient()
+ stripe_token = sc.create_token(card=card1)
+
+ r = self.client.post("/accounts/manage/", data={'stripe_token':stripe_token.id}, follow=True)
+
def test_good_bad_cc_scenarios(self):
self.good_cc_scenario()
self.bad_cc_scenario()
-
+ self.recharge_with_new_card()
# look at emails generated through these scenarios
#print len(mail.outbox)
@@ -341,5 +356,8 @@ class UnifiedCampaignTests(TestCase):
self.assertEqual(len(mail.outbox), 7)
-
-
+ # list all transactions
+
+ for t in Transaction.objects.all():
+ print t.id, t.status
+
diff --git a/payment/models.py b/payment/models.py
index 9795d00d..eb1240c7 100644
--- a/payment/models.py
+++ b/payment/models.py
@@ -3,7 +3,6 @@ from django.contrib.auth.models import User
from django.conf import settings
from django.db.models import Q
-import regluit.payment.manager
from regluit.payment.parameters import *
from regluit.payment.signals import credit_balance_added, pledge_created
from regluit.utils.localdatetime import now
@@ -357,22 +356,30 @@ def handle_transaction_delete(sender, instance, **kwargs):
post_save.connect(handle_transaction_change,sender=Transaction)
post_delete.connect(handle_transaction_delete,sender=Transaction)
+# handle recharging failed transactions
+
def recharge_failed_transactions(sender, created, instance, **kwargs):
"""When a new Account is saved, check whether this is the new active account for a user. If so, recharge any
outstanding failed transactions
"""
-
- transactions_to_recharge = instance.user.transaction_set.filter((Q(status=TRANSACTION_STATUS_FAILED) | Q(status=TRANSACTION_STATUS_ERROR)) & Q(campaign__status='UNSUCCESSFUL')).all()
+
+ # make sure the new account is active
+ if instance.date_deactivated is not None:
+ return False
+
+ transactions_to_recharge = instance.user.transaction_set.filter((Q(status=TRANSACTION_STATUS_FAILED) | Q(status=TRANSACTION_STATUS_ERROR)) & Q(campaign__status='SUCCESSFUL')).all()
if transactions_to_recharge:
- pm = manager.PaymentManager()
+ from regluit.payment.manager import PaymentManager
+ pm = PaymentManager()
for transaction in transactions_to_recharge:
# check whether we are still within the window to recharge
if (now() - transaction.campaign.deadline) < datetime.timedelta(settings.RECHARGE_WINDOW):
- pm.execute_transaction(transaction)
+ logger.info("Recharging transaction {0} w/ status {1}".format(transaction.id, transaction.status))
+ pm.execute_transaction(transaction, [])
post_save.connect(recharge_failed_transactions, sender=Account)
-# handle recharging failed transactions
+
From ec09588650f92f5f2793fc8dc5bdcd1d5d7cc6e2 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Wed, 14 Nov 2012 11:39:56 -0800
Subject: [PATCH 23/33] Add a (currently failing) test test_stripe_token_none
---
frontend/tests.py | 40 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 39 insertions(+), 1 deletion(-)
diff --git a/frontend/tests.py b/frontend/tests.py
index 672fe4c0..c7bf7aa2 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -360,4 +360,42 @@ class UnifiedCampaignTests(TestCase):
for t in Transaction.objects.all():
print t.id, t.status
-
+
+ def test_stripe_token_none(self):
+ # how much of test.campaigntest.test_relaunch can be done here?
+ username="RaymondYee"
+ password="Test_Password_"
+ work_id =1
+ preapproval_amount='10'
+ premium_id='150'
+
+ self.assertTrue(self.client.login(username=username, password=password))
+
+ # Pro Web 2.0 Mashups
+ self.work = Work.objects.get(id=work_id)
+ r = self.client.get("/work/%s/" % (self.work.id))
+
+ r = self.client.get("/work/%s/" % self.work.id)
+ self.assertEqual(r.status_code, 200)
+
+ # go to pledge page
+ r = self.client.get("/pledge/%s" % self.work.id, data={}, follow=True)
+ self.assertEqual(r.status_code, 200)
+
+ # submit to pledge page
+ r = self.client.post("/pledge/%s/" % self.work.id, data={'preapproval_amount': preapproval_amount,
+ 'premium_id':premium_id}, follow=True)
+ self.assertEqual(r.status_code, 200)
+
+ # Should now be on the fund page
+ pledge_fund_path = r.request.get('PATH_INFO')
+ self.assertTrue(pledge_fund_path.startswith('/pledge/fund'))
+ # pull out the transaction info
+ t_id = int(pledge_fund_path.replace('/pledge/fund/',''))
+
+ stripe_token = ''
+ r = self.client.post(pledge_fund_path, data={'stripe_token':stripe_token}, follow=True)
+ print r.status_code, r.request.get('PATH_INFO')
+ print dir(r)
+
+
From 4fa36ad30e9ea4ffa9b8e2748ef4a26eb43120bf Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Wed, 14 Nov 2012 12:14:06 -0800
Subject: [PATCH 24/33] test_stripe_token_none passes now
---
frontend/tests.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/frontend/tests.py b/frontend/tests.py
index c7bf7aa2..54a6f7ff 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -362,7 +362,8 @@ class UnifiedCampaignTests(TestCase):
print t.id, t.status
def test_stripe_token_none(self):
- # how much of test.campaigntest.test_relaunch can be done here?
+ """Test that if an empty stripe_token is submitted to pledge page, we catch that issue and present normal error page to user"""
+
username="RaymondYee"
password="Test_Password_"
work_id =1
@@ -394,8 +395,9 @@ class UnifiedCampaignTests(TestCase):
t_id = int(pledge_fund_path.replace('/pledge/fund/',''))
stripe_token = ''
+
r = self.client.post(pledge_fund_path, data={'stripe_token':stripe_token}, follow=True)
- print r.status_code, r.request.get('PATH_INFO')
- print dir(r)
+ self.assertEqual(r.status_code, 200)
+
From 7b75c36390a101497480bfbdeec53c9f550b8f91 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Wed, 14 Nov 2012 14:33:37 -0800
Subject: [PATCH 25/33] Now UnifiedCampaignTests passing -- and number of
emails sent out align with expectation
---
frontend/tests.py | 63 +++++++++++++++++++++++++++++------------------
1 file changed, 39 insertions(+), 24 deletions(-)
diff --git a/frontend/tests.py b/frontend/tests.py
index 54a6f7ff..416d6e8e 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -331,41 +331,39 @@ class UnifiedCampaignTests(TestCase):
card1 = card(number=TEST_CARDS[0][0], exp_month=1, exp_year='2020', cvc='123', name='dataunbound',
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
+ # track start time and end time of these stipe interactions so that we can limit the window of Events to look for
+ time0 = time.time()
+
sc = StripeClient()
stripe_token = sc.create_token(card=card1)
r = self.client.post("/accounts/manage/", data={'stripe_token':stripe_token.id}, follow=True)
+ time1 = time.time()
+
+ # retrieve events from this period -- need to pass in ints for event creation times
+ events = list(sc._all_objs('Event', created={'gte':int(time0), 'lte':int(time1+1.0)}))
+
+ # now feed each of the events to the IPN processor.
+ ipn_url = reverse("HandleIPN", args=('stripelib',))
+
+ for (i, event) in enumerate(events):
+ r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
+ self.assertEqual(r.status_code, 200)
+
+
def test_good_bad_cc_scenarios(self):
self.good_cc_scenario()
self.bad_cc_scenario()
self.recharge_with_new_card()
-
- # look at emails generated through these scenarios
- #print len(mail.outbox)
- #for (i, m) in enumerate(mail.outbox):
- # print i, m.subject
-
-#0 [localhost:8000] Thank you for supporting Pro Web 2.0 Mashups at Unglue.it!
-#1 [localhost:8000] Thanks to you, the campaign for Pro Web 2.0 Mashups has succeeded!
-#2 Stripe Customer (id cus_0gf9OjXHDhBwjw; description: RaymondYee) created
-#3 [localhost:8000] Thank you for supporting Moby Dick at Unglue.it!
-#4 [localhost:8000] Someone new has wished for your work at Unglue.it
-#5 [localhost:8000] Thanks to you, the campaign for Moby Dick has succeeded! However, your credit card charge failed.
-#6 Stripe Customer (id cus_0gf93tJp036kmG; description: dataunbound) created
-
- self.assertEqual(len(mail.outbox), 7)
-
- # list all transactions
-
- for t in Transaction.objects.all():
- print t.id, t.status
+ self.stripe_token_none()
+ self.confirm_num_mail()
- def test_stripe_token_none(self):
+ def stripe_token_none(self):
"""Test that if an empty stripe_token is submitted to pledge page, we catch that issue and present normal error page to user"""
- username="RaymondYee"
- password="Test_Password_"
+ username="hmelville"
+ password="gofish!"
work_id =1
preapproval_amount='10'
premium_id='150'
@@ -399,5 +397,22 @@ class UnifiedCampaignTests(TestCase):
r = self.client.post(pledge_fund_path, data={'stripe_token':stripe_token}, follow=True)
self.assertEqual(r.status_code, 200)
+
+ def confirm_num_mail(self):
+ # look at emails generated through these scenarios
+ #print len(mail.outbox)
+ #for (i, m) in enumerate(mail.outbox):
+ # print i, m.subject
+
+ self.assertEqual(len(mail.outbox), 9)
+
+#0 [localhost:8000] Thank you for supporting Pro Web 2.0 Mashups at Unglue.it!
+#1 [localhost:8000] Thanks to you, the campaign for Pro Web 2.0 Mashups has succeeded!
+#2 Stripe Customer (id cus_0ji1hFS8xLluuZ; description: RaymondYee) created
+#3 [localhost:8000] Thank you for supporting Moby Dick at Unglue.it!
+#4 [localhost:8000] Someone new has wished for your work at Unglue.it
+#5 [localhost:8000] Thanks to you, the campaign for Moby Dick has succeeded! However, your credit card charge failed.
+#6 Stripe Customer (id cus_0ji2Cmu6sXKBCi; description: dataunbound) created
+#7 [localhost:8000] Thanks to you, the campaign for Moby Dick has succeeded!
+#8 Stripe Customer (id cus_0ji24dPDiFGWU2; description: dataunbound) created
-
From b02a289e019a1e4d1c642887a0e36157ae94011d Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Thu, 15 Nov 2012 09:06:55 -0800
Subject: [PATCH 26/33] Add templates for pledge_failed notification
---
.../notification/pledge_failed/full.txt | 16 ++++++++++
.../notification/pledge_failed/notice.html | 31 +++++++++++++++++++
.../notification/pledge_failed/short.txt | 1 +
3 files changed, 48 insertions(+)
create mode 100644 frontend/templates/notification/pledge_failed/full.txt
create mode 100644 frontend/templates/notification/pledge_failed/notice.html
create mode 100644 frontend/templates/notification/pledge_failed/short.txt
diff --git a/frontend/templates/notification/pledge_failed/full.txt b/frontend/templates/notification/pledge_failed/full.txt
new file mode 100644
index 00000000..4876a6d9
--- /dev/null
+++ b/frontend/templates/notification/pledge_failed/full.txt
@@ -0,0 +1,16 @@
+{% load humanize %}Congratulations!
+
+Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. Our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed.
+
+Pledge summary
+{% include "notification/pledge_summary.txt" %}
+
+We will notify you when the unglued ebook is available for you to read. If you've requested special premiums, the rights holder, {{ transaction.campaign.rightsholder }}, will be in touch with you via email to request any information needed to deliver your premium.
+
+If you'd like to visit the campaign page, click here:
+https://{{ site.domain }}{% url work transaction.campaign.work.id %}
+
+Thank you again for your support.
+
+{{ transaction.campaign.rightsholder }} and the Unglue.it team
+
diff --git a/frontend/templates/notification/pledge_failed/notice.html b/frontend/templates/notification/pledge_failed/notice.html
new file mode 100644
index 00000000..4836cfb4
--- /dev/null
+++ b/frontend/templates/notification/pledge_failed/notice.html
@@ -0,0 +1,31 @@
+{% extends "notification/notice_template.html" %}
+{% load humanize %}
+
+{% block comments_book %}
+
+{% endblock %}
+
+{% block comments_graphical %}
+ Hooray! The campaign for {{ transaction.campaign.work.title }} has succeeded. Our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed. Thank you again for your help.
+{% endblock %}
+
+{% block comments_textual %}
+ Congratulations!
+
+ Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition.
+ Our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed.
+
+ Pledge Summary
+ Amount pledged: {{ transaction.amount|intcomma }}
+ Premium: {{ transaction.premium.description }}
+
+ We will notify you when the unglued ebook is available for you to read. If you've requested special premiums, the rights holder, {{ transaction.campaign.rightsholder }}, will be in touch with you via email to request any information needed to deliver your premium.
+
+ For more information, visit the visit the campaign page.
+
+
+ Thank you again for your support.
+
+ {{ transaction.campaign.rightsholder }} and the Unglue.it team
+
+{% endblock %}
\ No newline at end of file
diff --git a/frontend/templates/notification/pledge_failed/short.txt b/frontend/templates/notification/pledge_failed/short.txt
new file mode 100644
index 00000000..b199122f
--- /dev/null
+++ b/frontend/templates/notification/pledge_failed/short.txt
@@ -0,0 +1 @@
+Thanks to you, the campaign for {{transaction.campaign.work.title}} has succeeded! However, your credit card charge failed.
\ No newline at end of file
From c034e10b3045f8da453ed0dd131853ac89c9e2b5 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Tue, 20 Nov 2012 09:28:10 -0800
Subject: [PATCH 27/33] Putting in some code to record a PaymentResponse to
track errors in stripelib.Execute
---
frontend/tests.py | 8 ++++----
payment/stripelib.py | 15 ++++++++-------
2 files changed, 12 insertions(+), 11 deletions(-)
diff --git a/frontend/tests.py b/frontend/tests.py
index 416d6e8e..b1a0a4e1 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -400,11 +400,11 @@ class UnifiedCampaignTests(TestCase):
def confirm_num_mail(self):
# look at emails generated through these scenarios
- #print len(mail.outbox)
- #for (i, m) in enumerate(mail.outbox):
- # print i, m.subject
+ print len(mail.outbox)
+ for (i, m) in enumerate(mail.outbox):
+ print i, m.subject
- self.assertEqual(len(mail.outbox), 9)
+ #self.assertEqual(len(mail.outbox), 9)
#0 [localhost:8000] Thank you for supporting Pro Web 2.0 Mashups at Unglue.it!
#1 [localhost:8000] Thanks to you, the campaign for Pro Web 2.0 Mashups has succeeded!
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 5cc2cd2b..9f364045 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -619,8 +619,7 @@ class Processor(baseprocessor.Processor):
class Execute(StripePaymentRequest):
'''
- The Execute function sends an existing token(generated via the URL from the pay operation), and collects
- the money.
+ The Execute function attempts to charge the credit card of stripe Customer associated with user connected to transaction
'''
def __init__(self, transaction=None):
@@ -648,11 +647,13 @@ class Processor(baseprocessor.Processor):
"tc.id": transaction.campaign.id,
"amount": float(transaction.amount)}))
except stripe.StripeError as e:
- # what to record in terms of errors?
+ # what to record in terms of errors? (error log?)
+ # use PaymentResponse to store error
- transaction.status = TRANSACTION_STATUS_ERROR
- transaction.error = e.message
- transaction.save()
+ r = PaymentResponse.objects.create(api="stripelib.Execute", correlation_id=None,
+ timestamp=now(), info=e.message, status=TRANSACTION_STATUS_ERROR, transaction=transaction)
+
+ # what transaction status to set?
raise StripelibError(e.message, e)
@@ -665,7 +666,7 @@ class Processor(baseprocessor.Processor):
transaction.save()
else:
# nothing to charge
- raise StripeLibError("No customer id available to charge tor transaction {0}".format(transaction.id), None)
+ raise StripeLibError("No customer id available to charge for transaction {0}".format(transaction.id), None)
def api(self):
From ce6e7f865df5d0b24855808b963be03d2d07b886 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Tue, 20 Nov 2012 13:56:14 -0800
Subject: [PATCH 28/33] tests pass on this commit
---
frontend/tests.py | 8 ++++----
payment/models.py | 2 +-
payment/stripelib.py | 11 +++++++----
3 files changed, 12 insertions(+), 9 deletions(-)
diff --git a/frontend/tests.py b/frontend/tests.py
index b1a0a4e1..416d6e8e 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -400,11 +400,11 @@ class UnifiedCampaignTests(TestCase):
def confirm_num_mail(self):
# look at emails generated through these scenarios
- print len(mail.outbox)
- for (i, m) in enumerate(mail.outbox):
- print i, m.subject
+ #print len(mail.outbox)
+ #for (i, m) in enumerate(mail.outbox):
+ # print i, m.subject
- #self.assertEqual(len(mail.outbox), 9)
+ self.assertEqual(len(mail.outbox), 9)
#0 [localhost:8000] Thank you for supporting Pro Web 2.0 Mashups at Unglue.it!
#1 [localhost:8000] Thanks to you, the campaign for Pro Web 2.0 Mashups has succeeded!
diff --git a/payment/models.py b/payment/models.py
index eb1240c7..ed9f3715 100644
--- a/payment/models.py
+++ b/payment/models.py
@@ -366,7 +366,7 @@ def recharge_failed_transactions(sender, created, instance, **kwargs):
# make sure the new account is active
if instance.date_deactivated is not None:
return False
-
+
transactions_to_recharge = instance.user.transaction_set.filter((Q(status=TRANSACTION_STATUS_FAILED) | Q(status=TRANSACTION_STATUS_ERROR)) & Q(campaign__status='SUCCESSFUL')).all()
if transactions_to_recharge:
diff --git a/payment/stripelib.py b/payment/stripelib.py
index 9f364045..b3b1925a 100644
--- a/payment/stripelib.py
+++ b/payment/stripelib.py
@@ -12,7 +12,7 @@ from django.conf import settings
from django.http import HttpResponse
from django.core.mail import send_mail
-from regluit.payment.models import Account, Transaction
+from regluit.payment.models import Account, Transaction, PaymentResponse
from regluit.payment.parameters import PAYMENT_HOST_STRIPE
from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_ERROR, PAYMENT_TYPE_AUTHORIZATION, TRANSACTION_STATUS_CANCELED
from regluit.payment import baseprocessor
@@ -651,10 +651,13 @@ class Processor(baseprocessor.Processor):
# 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)
-
- # what transaction status to set?
+ timestamp=now(), info=e.message,
+ status=TRANSACTION_STATUS_ERROR, transaction=transaction)
+ transaction.status = TRANSACTION_STATUS_ERROR
+ transaction.error = e.message
+ transaction.save()
+
raise StripelibError(e.message, e)
else:
From cbac6f1e3c76f7c4209cc096f074a7f4ca8ac2fb Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Wed, 21 Nov 2012 09:21:01 -0800
Subject: [PATCH 29/33] First round of edits to get pledge_charged and
pledge_failed notices to be parallel. tests passing
---
core/models.py | 19 ++++++++++++-------
.../notification/pledge_charged/full.txt | 2 +-
.../notification/pledge_charged/notice.html | 4 ++--
.../notification/pledge_charged/short.txt | 2 +-
.../notification/pledge_failed/full.txt | 7 +++----
.../notification/pledge_failed/notice.html | 5 +++--
.../notification/pledge_failed/short.txt | 2 +-
payment/manager.py | 16 +++++++++-------
8 files changed, 32 insertions(+), 25 deletions(-)
diff --git a/core/models.py b/core/models.py
index 43531687..956dc00b 100755
--- a/core/models.py
+++ b/core/models.py
@@ -301,24 +301,29 @@ class Campaign(models.Model):
self.save()
action = CampaignAction(campaign=self, type='succeeded', comment = self.current_total)
action.save()
- if send_notice:
- successful_campaign.send(sender=None,campaign=self)
+
if process_transactions:
p = PaymentManager()
results = p.execute_campaign(self)
- # should be more sophisticated in whether to return True -- look at all the transactions
+
+ if send_notice:
+ successful_campaign.send(sender=None,campaign=self)
+
+ # should be more sophisticated in whether to return True -- look at all the transactions?
return True
elif self.deadline < now() and self.current_total < self.target:
self.status = 'UNSUCCESSFUL'
self.save()
action = CampaignAction(campaign=self, type='failed', comment = self.current_total)
action.save()
- if send_notice:
- regluit.core.signals.unsuccessful_campaign.send(sender=None,campaign=self)
+
if process_transactions:
p = PaymentManager()
- results = p.cancel_campaign(self)
- # should be more sophisticated in whether to return True -- look at all the transactions
+ results = p.cancel_campaign(self)
+
+ if send_notice:
+ regluit.core.signals.unsuccessful_campaign.send(sender=None,campaign=self)
+ # should be more sophisticated in whether to return True -- look at all the transactions?
return True
else:
return False
diff --git a/frontend/templates/notification/pledge_charged/full.txt b/frontend/templates/notification/pledge_charged/full.txt
index 764d6596..9e42f8b6 100644
--- a/frontend/templates/notification/pledge_charged/full.txt
+++ b/frontend/templates/notification/pledge_charged/full.txt
@@ -1,6 +1,6 @@
{% load humanize %}Congratulations!
-Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. Your credit card will be charged ${{ transaction.amount|intcomma }}.
+Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. Your credit card has been charged ${{ transaction.amount|intcomma }}.
Pledge summary
{% include "notification/pledge_summary.txt" %}
diff --git a/frontend/templates/notification/pledge_charged/notice.html b/frontend/templates/notification/pledge_charged/notice.html
index 07e6ae28..f8996f74 100644
--- a/frontend/templates/notification/pledge_charged/notice.html
+++ b/frontend/templates/notification/pledge_charged/notice.html
@@ -6,13 +6,13 @@
{% endblock %}
{% block comments_graphical %}
- Hooray! The campaign for {{ transaction.campaign.work.title }} has succeeded. Your credit card is being charged ${{ transaction.amount }}. Thank you again for your help.
+ Hooray! The campaign for {{ transaction.campaign.work.title }} has succeeded. Your credit card has been charged ${{ transaction.amount }}. Thank you again for your help.
{% endblock %}
{% block comments_textual %}
Congratulations!
- Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. {{ transaction.host|capfirst }} will now charge your credit card.
+ Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. {{ transaction.host|capfirst }} has been charged to your credit card.
Pledge Summary
Amount pledged: {{ transaction.amount|intcomma }}
diff --git a/frontend/templates/notification/pledge_charged/short.txt b/frontend/templates/notification/pledge_charged/short.txt
index 7cf0e3f8..58ed6641 100644
--- a/frontend/templates/notification/pledge_charged/short.txt
+++ b/frontend/templates/notification/pledge_charged/short.txt
@@ -1 +1 @@
-Thanks to you, the campaign for {{transaction.campaign.work.title}} has succeeded!
\ No newline at end of file
+Your pledge to the campaign for {{transaction.campaign.work.title}} has been charged.
\ No newline at end of file
diff --git a/frontend/templates/notification/pledge_failed/full.txt b/frontend/templates/notification/pledge_failed/full.txt
index 4876a6d9..03b014f0 100644
--- a/frontend/templates/notification/pledge_failed/full.txt
+++ b/frontend/templates/notification/pledge_failed/full.txt
@@ -1,12 +1,11 @@
-{% load humanize %}Congratulations!
+{% load humanize %}Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition.
-Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. Our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed.
+However, our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed. Please update your credit card information at
+https://{{ site.domain }}{%url manage_account%} within 7 days so that you can fulfill your pledge. Thank you!
Pledge summary
{% include "notification/pledge_summary.txt" %}
-We will notify you when the unglued ebook is available for you to read. If you've requested special premiums, the rights holder, {{ transaction.campaign.rightsholder }}, will be in touch with you via email to request any information needed to deliver your premium.
-
If you'd like to visit the campaign page, click here:
https://{{ site.domain }}{% url work transaction.campaign.work.id %}
diff --git a/frontend/templates/notification/pledge_failed/notice.html b/frontend/templates/notification/pledge_failed/notice.html
index 4836cfb4..37f28bdb 100644
--- a/frontend/templates/notification/pledge_failed/notice.html
+++ b/frontend/templates/notification/pledge_failed/notice.html
@@ -10,10 +10,11 @@
{% endblock %}
{% block comments_textual %}
-
Congratulations!
+ Your Attention Required!
Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition.
- Our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed.
+ However, our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed. Please update your credit card information at
+ https://{{ site.domain }}{%url manage_account%} within 7 days so that you can fulfill your pledge. Thank you!
Pledge Summary
Amount pledged: {{ transaction.amount|intcomma }}
diff --git a/frontend/templates/notification/pledge_failed/short.txt b/frontend/templates/notification/pledge_failed/short.txt
index b199122f..d7e39cf9 100644
--- a/frontend/templates/notification/pledge_failed/short.txt
+++ b/frontend/templates/notification/pledge_failed/short.txt
@@ -1 +1 @@
-Thanks to you, the campaign for {{transaction.campaign.work.title}} has succeeded! However, your credit card charge failed.
\ No newline at end of file
+We were unable to collect your pledge to the campaign for {{transaction.campaign.work.title}}.
\ No newline at end of file
diff --git a/payment/manager.py b/payment/manager.py
index 4836cde3..b02ec755 100644
--- a/payment/manager.py
+++ b/payment/manager.py
@@ -326,6 +326,8 @@ class PaymentManager( object ):
# only allow active transactions to go through again, if there is an error, intervention is needed
transactions = Transaction.objects.filter(campaign=campaign, status=TRANSACTION_STATUS_ACTIVE)
+ results = []
+
for t in transactions:
#
# Currently receivers are only used for paypal, so it is OK to leave the paypal info here
@@ -333,14 +335,14 @@ class PaymentManager( object ):
receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':t.amount},
{'email':campaign.paypal_receiver, 'amount':D(t.amount) * (D('1.00') - D(str(settings.GLUEJAR_COMMISSION)))}]
- self.execute_transaction(t, receiver_list)
+ try:
+ self.execute_transaction(t, receiver_list)
+ except Exception as e:
+ results.append(t, e)
+ else:
+ results.append(t, None)
- # TO DO: update campaign status
- # Should this be done first before executing the transactions?
- # How does the success/failure of transactions affect states of campaigns
-
-
- return transactions
+ return results
def finish_campaign(self, campaign):
'''
From ecea475419aaf3bf07f7c1ec2f167919e9475546 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Wed, 21 Nov 2012 11:40:19 -0800
Subject: [PATCH 30/33] a few modifications to pledge_failed notice
---
core/signals.py | 12 ++++++++++--
.../templates/notification/pledge_failed/full.txt | 4 ++--
.../templates/notification/pledge_failed/notice.html | 6 +++---
frontend/tests.py | 9 ++++++++-
payment/manager.py | 4 ++--
5 files changed, 25 insertions(+), 10 deletions(-)
diff --git a/core/signals.py b/core/signals.py
index 4699732f..8e710152 100644
--- a/core/signals.py
+++ b/core/signals.py
@@ -8,6 +8,9 @@ from django.contrib.sites.models import Site
from django.conf import settings
from django.utils.translation import ugettext_noop as _
+import datetime
+from regluit.utils.localdatetime import now
+
from notification import models as notification
from social_auth.signals import pre_update
@@ -175,11 +178,16 @@ transaction_charged.connect(handle_transaction_charged)
# dealing with failed transactions
def handle_transaction_failed(sender,transaction=None, **kwargs):
- if transaction==None:
+ if transaction is None:
return
+
+ # window for recharging
+ recharge_deadline = transaction.campaign.deadline + datetime.timedelta(settings.RECHARGE_WINDOW)
+
notification.queue([transaction.user], "pledge_failed", {
'site':Site.objects.get_current(),
- 'transaction':transaction
+ 'transaction':transaction,
+ 'recharge_deadline': recharge_deadline
}, True)
from regluit.core.tasks import emit_notifications
emit_notifications.delay()
diff --git a/frontend/templates/notification/pledge_failed/full.txt b/frontend/templates/notification/pledge_failed/full.txt
index 03b014f0..e4007fc5 100644
--- a/frontend/templates/notification/pledge_failed/full.txt
+++ b/frontend/templates/notification/pledge_failed/full.txt
@@ -1,7 +1,7 @@
{% load humanize %}Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition.
-However, our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed. Please update your credit card information at
-https://{{ site.domain }}{%url manage_account%} within 7 days so that you can fulfill your pledge. Thank you!
+However, our attempt to charge your credit card in the amount of ${{ transaction.amount|intcomma }} failed ({{transaction.error}}). Please update your credit card information at
+https://{{ site.domain }}{%url manage_account%} by {{recharge_deadline}} so that you can fulfill your pledge. Thank you!
Pledge summary
{% include "notification/pledge_summary.txt" %}
diff --git a/frontend/templates/notification/pledge_failed/notice.html b/frontend/templates/notification/pledge_failed/notice.html
index 37f28bdb..a9dce3e9 100644
--- a/frontend/templates/notification/pledge_failed/notice.html
+++ b/frontend/templates/notification/pledge_failed/notice.html
@@ -6,7 +6,7 @@
{% endblock %}
{% block comments_graphical %}
- Hooray! The campaign for {{ transaction.campaign.work.title }} has succeeded. Our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed. Thank you again for your help.
+ The campaign for {{ transaction.campaign.work.title }} has succeeded. Our attempt to charge your credit card in the amount of n${{ transaction.amount|intcomma }} failed ({{transaction.error}}). Thank you again for your help.
{% endblock %}
{% block comments_textual %}
@@ -14,7 +14,7 @@
Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition.
However, our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed. Please update your credit card information at
- https://{{ site.domain }}{%url manage_account%} within 7 days so that you can fulfill your pledge. Thank you!
+ https://{{ site.domain }}{%url manage_account%} by {{recharge_deadline}} so that you can fulfill your pledge. Thank you!
Pledge Summary
Amount pledged: {{ transaction.amount|intcomma }}
@@ -29,4 +29,4 @@
{{ transaction.campaign.rightsholder }} and the Unglue.it team
-{% endblock %}
\ No newline at end of file
+{% endblock %}
\ No newline at end of file
diff --git a/frontend/tests.py b/frontend/tests.py
index 416d6e8e..1b2e1238 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -403,9 +403,16 @@ class UnifiedCampaignTests(TestCase):
#print len(mail.outbox)
#for (i, m) in enumerate(mail.outbox):
# print i, m.subject
+ # if i in [5]:
+ # print m.body
self.assertEqual(len(mail.outbox), 9)
-
+
+ # print out notices and eventually write tests here to check expected
+
+ #from notification.models import Notice
+ #print [(n.id, n.notice_type.label) for n in Notice.objects.all()]
+
#0 [localhost:8000] Thank you for supporting Pro Web 2.0 Mashups at Unglue.it!
#1 [localhost:8000] Thanks to you, the campaign for Pro Web 2.0 Mashups has succeeded!
#2 Stripe Customer (id cus_0ji1hFS8xLluuZ; description: RaymondYee) created
diff --git a/payment/manager.py b/payment/manager.py
index b02ec755..c89c2057 100644
--- a/payment/manager.py
+++ b/payment/manager.py
@@ -338,9 +338,9 @@ class PaymentManager( object ):
try:
self.execute_transaction(t, receiver_list)
except Exception as e:
- results.append(t, e)
+ results.append((t, e))
else:
- results.append(t, None)
+ results.append((t, None))
return results
From 28eca2999c7120c4cc85783393ff895043a91fe8 Mon Sep 17 00:00:00 2001
From: Raymond Yee
Date: Wed, 21 Nov 2012 16:42:04 -0800
Subject: [PATCH 31/33] put checks for notices in proper context
---
frontend/tests.py | 30 +++++++++++++++++++++++++-----
1 file changed, 25 insertions(+), 5 deletions(-)
diff --git a/frontend/tests.py b/frontend/tests.py
index 1b2e1238..db4500b4 100755
--- a/frontend/tests.py
+++ b/frontend/tests.py
@@ -7,12 +7,13 @@ from django.core.urlresolvers import reverse
from django.conf import settings
from django.core import mail
-
from regluit.core.models import Work, Campaign, RightsHolder, Claim
from regluit.payment.models import Transaction
from regluit.payment.manager import PaymentManager
from regluit.payment.stripelib import StripeClient, TEST_CARDS, ERROR_TESTING, card
+from notification.models import Notice
+
from decimal import Decimal as D
from regluit.utils.localdatetime import now
from datetime import timedelta
@@ -266,7 +267,7 @@ class UnifiedCampaignTests(TestCase):
time1 = time.time()
# retrieve events from this period -- need to pass in ints for event creation times
- events = list(sc._all_objs('Event', created={'gte':int(time0), 'lte':int(time1+1.0)}))
+ events = list(sc._all_objs('Event', created={'gte':int(time0-1.0), 'lte':int(time1+1.0)}))
return (events, charge_exception)
@@ -294,6 +295,12 @@ class UnifiedCampaignTests(TestCase):
for (i, event) in enumerate(events):
r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
self.assertEqual(r.status_code, 200)
+
+ # expected notices
+
+ self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_you_have_pledged', recipient__username='RaymondYee')), 1)
+ self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_charged', recipient__username='RaymondYee')), 1)
+
def bad_cc_scenario(self):
"""Goal of this scenario: enter a CC that will cause a charge.failed event, have user repledge succesfully"""
@@ -320,6 +327,9 @@ class UnifiedCampaignTests(TestCase):
r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
self.assertEqual(r.status_code, 200)
+ self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_you_have_pledged', recipient__username='dataunbound')), 1)
+ self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_failed', recipient__username='dataunbound')), 1)
+
def recharge_with_new_card(self):
# mark campaign as SUCCESSFUL -- campaign for work 2
@@ -342,7 +352,7 @@ class UnifiedCampaignTests(TestCase):
time1 = time.time()
# retrieve events from this period -- need to pass in ints for event creation times
- events = list(sc._all_objs('Event', created={'gte':int(time0), 'lte':int(time1+1.0)}))
+ events = list(sc._all_objs('Event', created={'gte':int(time0-1.0), 'lte':int(time1+1.0)}))
# now feed each of the events to the IPN processor.
ipn_url = reverse("HandleIPN", args=('stripelib',))
@@ -350,6 +360,9 @@ class UnifiedCampaignTests(TestCase):
for (i, event) in enumerate(events):
r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
self.assertEqual(r.status_code, 200)
+
+ # a charge should now go through
+ self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_charged', recipient__username='dataunbound')), 1)
def test_good_bad_cc_scenarios(self):
@@ -409,9 +422,16 @@ class UnifiedCampaignTests(TestCase):
self.assertEqual(len(mail.outbox), 9)
# print out notices and eventually write tests here to check expected
-
+
#from notification.models import Notice
- #print [(n.id, n.notice_type.label) for n in Notice.objects.all()]
+ #print [(n.id, n.notice_type.label, n.recipient, n.added) for n in Notice.objects.all()]
+
+#[(6L, u'pledge_charged', , datetime.datetime(2012, 11, 21, 18, 33, 15)),
+#(5L, u'pledge_failed', , datetime.datetime(2012, 11, 21, 18, 33, 10)),
+#(4L, u'new_wisher', , datetime.datetime(2012, 11, 21, 18, 33, 8)),
+#(3L, u'pledge_you_have_pledged', , datetime.datetime(2012, 11, 21, 18, 33, 7)),
+#(2L, u'pledge_charged', , datetime.datetime(2012, 11, 21, 18, 33, 3)),
+#(1L, u'pledge_you_have_pledged', , datetime.datetime(2012, 11, 21, 18, 32, 56))]
#0 [localhost:8000] Thank you for supporting Pro Web 2.0 Mashups at Unglue.it!
#1 [localhost:8000] Thanks to you, the campaign for Pro Web 2.0 Mashups has succeeded!
From 1976bb14d3757dd6066b5e8aa0a7cdb7e512162a Mon Sep 17 00:00:00 2001
From: thatandromeda
Date: Mon, 26 Nov 2012 13:45:12 -0500
Subject: [PATCH 32/33] Update
frontend/templates/notification/pledge_charged/short.txt
---
frontend/templates/notification/pledge_charged/short.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/templates/notification/pledge_charged/short.txt b/frontend/templates/notification/pledge_charged/short.txt
index 58ed6641..f4cf9fe6 100644
--- a/frontend/templates/notification/pledge_charged/short.txt
+++ b/frontend/templates/notification/pledge_charged/short.txt
@@ -1 +1 @@
-Your pledge to the campaign for {{transaction.campaign.work.title}} has been charged.
\ No newline at end of file
+Your pledge to the campaign to unglue {{transaction.campaign.work.title}} has been charged.
\ No newline at end of file
From 8692d2654068fd71263a7b521af904b231111cea Mon Sep 17 00:00:00 2001
From: Andromeda Yelton
Date: Mon, 26 Nov 2012 14:23:04 -0500
Subject: [PATCH 33/33] friendlifying
---
.../templates/notification/pledge_charged/notice.html | 2 +-
frontend/templates/notification/pledge_failed/full.txt | 4 ++--
.../templates/notification/pledge_failed/notice.html | 10 ++++++----
.../templates/notification/pledge_failed/short.txt | 2 +-
4 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/frontend/templates/notification/pledge_charged/notice.html b/frontend/templates/notification/pledge_charged/notice.html
index f8996f74..bb8128d4 100644
--- a/frontend/templates/notification/pledge_charged/notice.html
+++ b/frontend/templates/notification/pledge_charged/notice.html
@@ -6,7 +6,7 @@
{% endblock %}
{% block comments_graphical %}
- Hooray! The campaign for {{ transaction.campaign.work.title }} has succeeded. Your credit card has been charged ${{ transaction.amount }}. Thank you again for your help.
+ Hooray! The campaign for {{ transaction.campaign.work.title }} has succeeded. Your credit card has been charged ${{ transaction.amount|intcomma }}. Thank you again for your help.
{% endblock %}
{% block comments_textual %}
diff --git a/frontend/templates/notification/pledge_failed/full.txt b/frontend/templates/notification/pledge_failed/full.txt
index e4007fc5..61f0834e 100644
--- a/frontend/templates/notification/pledge_failed/full.txt
+++ b/frontend/templates/notification/pledge_failed/full.txt
@@ -1,7 +1,7 @@
{% load humanize %}Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition.
-However, our attempt to charge your credit card in the amount of ${{ transaction.amount|intcomma }} failed ({{transaction.error}}). Please update your credit card information at
-https://{{ site.domain }}{%url manage_account%} by {{recharge_deadline}} so that you can fulfill your pledge. Thank you!
+However, our attempt to charge your credit card in the amount of ${{ transaction.amount|intcomma }} failed ({{transaction.error}}). Don't worry -- typically this means the card on file for you is expired, and once you update your card information we'll be able to collect your pledge on behalf of {{ transaction.campaign.rightsholder }}. Please update your credit card information at
+https://{{ site.domain }}{% url manage_account %} by {{ recharge_deadline }} so that you can fulfill your pledge. Thank you!
Pledge summary
{% include "notification/pledge_summary.txt" %}
diff --git a/frontend/templates/notification/pledge_failed/notice.html b/frontend/templates/notification/pledge_failed/notice.html
index a9dce3e9..335fac63 100644
--- a/frontend/templates/notification/pledge_failed/notice.html
+++ b/frontend/templates/notification/pledge_failed/notice.html
@@ -6,15 +6,17 @@
{% endblock %}
{% block comments_graphical %}
- The campaign for {{ transaction.campaign.work.title }} has succeeded. Our attempt to charge your credit card in the amount of n${{ transaction.amount|intcomma }} failed ({{transaction.error}}). Thank you again for your help.
+ The campaign for {{ transaction.campaign.work.title }} has succeeded. However, our attempt to charge your pledge for ${{ transaction.amount|intcomma }} to your credit card failed ({{transaction.error}}). Will you help us fix that?
{% endblock %}
{% block comments_textual %}
- Your Attention Required!
+ Your attention needed!
Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition.
- However, our attempt to charge your credit card in the amount ${{ transaction.amount|intcomma }} failed. Please update your credit card information at
- https://{{ site.domain }}{%url manage_account%} by {{recharge_deadline}} so that you can fulfill your pledge. Thank you!
+ However, our attempt to charge your credit card for ${{ transaction.amount|intcomma }} failed.
+
+ Don't worry - normally this just means the card we have on file for you is expired. Once you've updated your card information we'll be able to collect your pledge on behalf of {{ transaction.campaign.rightsholder }}. Please update your credit card information at
+ https://{{ site.domain }}{% url manage_account %} by {{ recharge_deadline }} so that you can fulfill your pledge. Thank you!
Pledge Summary
Amount pledged: {{ transaction.amount|intcomma }}
diff --git a/frontend/templates/notification/pledge_failed/short.txt b/frontend/templates/notification/pledge_failed/short.txt
index d7e39cf9..3e9e1646 100644
--- a/frontend/templates/notification/pledge_failed/short.txt
+++ b/frontend/templates/notification/pledge_failed/short.txt
@@ -1 +1 @@
-We were unable to collect your pledge to the campaign for {{transaction.campaign.work.title}}.
\ No newline at end of file
+Attention needed: your pledge to unglue {{transaction.campaign.work.title}}
\ No newline at end of file