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':