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 %} + cover image for {{ transaction.campaign.work.title }} +{% 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